dependabot-nuget 0.80.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 07c225b6dcae09a617444df10ca1f5fc0aaf1ae159ba376d980c84d26a758f39
4
+ data.tar.gz: 7a304f35e363cc2a6f40b7c040bba03b14b848add5e5048e9bdbab8ae68c2aab
5
+ SHA512:
6
+ metadata.gz: da29d11282b5b11e47a33066c43172b82386203939acc7241496a05ca2b97a91a2aedab4c71123fe006e11bd0f745f14f52e2de5abf00164205917c34f401672
7
+ data.tar.gz: a5996018d925129d2940ee06c7b7382885d13b0ec6fb95b82c4d09fb0bd4987a8af97d5575d86892a8b59687ab25419015d55fc4d55b12b7766f8f656421a131
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "pathname"
5
+
6
+ require "dependabot/nuget/file_fetcher"
7
+
8
+ module Dependabot
9
+ module Nuget
10
+ class FileFetcher
11
+ class ImportPathsFinder
12
+ def initialize(project_file:)
13
+ @project_file = project_file
14
+ end
15
+
16
+ def import_paths
17
+ doc = Nokogiri::XML(project_file.content)
18
+ doc.remove_namespaces!
19
+ doc.xpath("/Project/Import").map do |import_node|
20
+ path = import_node.attribute("Project").value.strip.tr("\\", "/")
21
+ path = File.join(current_dir, path) unless current_dir.nil?
22
+ Pathname.new(path).cleanpath.to_path
23
+ end
24
+ end
25
+
26
+ def project_reference_paths
27
+ doc = Nokogiri::XML(project_file.content)
28
+ doc.remove_namespaces!
29
+ doc.xpath("/Project/ItemGroup/ProjectReference").map do |node|
30
+ path = node.attribute("Include").value.strip.tr("\\", "/")
31
+ path = File.join(current_dir, path) unless current_dir.nil?
32
+ Pathname.new(path).cleanpath.to_path
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :project_file
39
+
40
+ def current_dir
41
+ parts = project_file.name.split("/")[0..-2]
42
+ return if parts.empty?
43
+
44
+ parts.join("/")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "dependabot/nuget/file_fetcher"
5
+
6
+ module Dependabot
7
+ module Nuget
8
+ class FileFetcher
9
+ class SlnProjectPathsFinder
10
+ PROJECT_PATH_REGEX =
11
+ /(?<=["'])[^"']*?\.(?:vb|cs|fs)proj(?=["'])/.freeze
12
+
13
+ def initialize(sln_file:)
14
+ @sln_file = sln_file
15
+ end
16
+
17
+ def project_paths
18
+ paths = []
19
+ sln_file_lines = sln_file.content.lines
20
+
21
+ sln_file_lines.each_with_index do |line, index|
22
+ next unless line.match?(/^\s*Project/)
23
+
24
+ # Don't know how to handle multi-line project declarations yet
25
+ next unless sln_file_lines[index + 1]&.match?(/^\s*EndProject/)
26
+
27
+ path = line.split('"')[5]
28
+ path = path.tr("\\", "/")
29
+
30
+ # If the path doesn't have an extension it's probably a directory
31
+ next unless path.match?(/\.[a-z]{2}proj$/)
32
+
33
+ path = File.join(current_dir, path) unless current_dir.nil?
34
+ paths << Pathname.new(path).cleanpath.to_path
35
+ end
36
+
37
+ paths
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :sln_file
43
+
44
+ def current_dir
45
+ parts = sln_file.name.split("/")[0..-2]
46
+ return if parts.empty?
47
+
48
+ parts.join("/")
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/file_fetchers"
4
+ require "dependabot/file_fetchers/base"
5
+
6
+ module Dependabot
7
+ module Nuget
8
+ class FileFetcher < Dependabot::FileFetchers::Base
9
+ require_relative "file_fetcher/import_paths_finder"
10
+ require_relative "file_fetcher/sln_project_paths_finder"
11
+
12
+ def self.required_files_in?(filenames)
13
+ return true if filenames.any? { |f| f.match?(/^packages\.config$/i) }
14
+ return true if filenames.any? { |f| f.end_with?(".sln") }
15
+
16
+ filenames.any? { |name| name.match?(%r{^[^/]*\.[a-z]{2}proj$}) }
17
+ end
18
+
19
+ def self.required_files_message
20
+ "Repo must contain a .(cs|vb|fs)proj file or a packages.config."
21
+ end
22
+
23
+ private
24
+
25
+ def fetch_files
26
+ fetched_files = []
27
+ fetched_files += project_files
28
+ fetched_files += directory_build_props_files
29
+ fetched_files += imported_property_files
30
+
31
+ fetched_files += packages_config_files
32
+ fetched_files << nuget_config if nuget_config
33
+
34
+ fetched_files = fetched_files.uniq
35
+
36
+ if project_files.none? && packages_config_files.none?
37
+ raise(
38
+ Dependabot::DependencyFileNotFound,
39
+ File.join(directory, "<anything>.(cs|vb|fs)proj")
40
+ )
41
+ end
42
+
43
+ fetched_files
44
+ end
45
+
46
+ def project_files
47
+ @project_files ||=
48
+ begin
49
+ project_files = []
50
+ project_files << csproj_file if csproj_file
51
+ project_files << vbproj_file if vbproj_file
52
+ project_files << fsproj_file if fsproj_file
53
+
54
+ project_files += sln_project_files
55
+ project_files
56
+ end
57
+ rescue Octokit::NotFound, Gitlab::Error::NotFound
58
+ raise(
59
+ Dependabot::DependencyFileNotFound,
60
+ File.join(directory, "<anything>.(cs|vb|fs)proj")
61
+ )
62
+ end
63
+
64
+ def packages_config_files
65
+ return @packages_config_files if @packages_config_files
66
+
67
+ candidate_paths =
68
+ [*project_files.map { |f| File.dirname(f.name) }, "."].uniq
69
+
70
+ @packages_config_files ||=
71
+ candidate_paths.map do |dir|
72
+ file = repo_contents(dir: dir).
73
+ find { |f| f.name.casecmp("packages.config").zero? }
74
+ fetch_file_from_host(File.join(dir, file.name)) if file
75
+ end.compact
76
+ end
77
+
78
+ def sln_file
79
+ return unless sln_file_name
80
+
81
+ @sln_file ||= fetch_file_from_host(sln_file_name)
82
+ end
83
+
84
+ def sln_file_name
85
+ sln_files = repo_contents.select { |f| f.name.end_with?(".sln") }
86
+
87
+ # If there are no sln files, just return `nil`
88
+ return if sln_files.none?
89
+
90
+ # Use the biggest sln file
91
+ sln_files.max_by(&:size).name
92
+ end
93
+
94
+ def directory_build_props_files
95
+ return @directory_build_props_files if @directory_build_checked
96
+
97
+ @directory_build_checked = true
98
+ attempted_paths = []
99
+ @directory_build_props_files = []
100
+
101
+ # Don't need to insert "." here, because Directory.Build.props files
102
+ # can only be used by project files (not packages.config ones)
103
+ project_files.map { |f| File.dirname(f.name) }.uniq.map do |dir|
104
+ possible_paths = dir.split("/").map.with_index do |_, i|
105
+ base = dir.split("/").first(i + 1).join("/")
106
+ Pathname.new(base + "/Directory.Build.props").cleanpath.to_path
107
+ end.reverse + ["Directory.Build.props"]
108
+
109
+ possible_paths.each do |path|
110
+ break if attempted_paths.include?(path)
111
+
112
+ attempted_paths << path
113
+ @directory_build_props_files << fetch_file_from_host(path)
114
+ rescue Dependabot::DependencyFileNotFound
115
+ next
116
+ end
117
+ end
118
+
119
+ @directory_build_props_files
120
+ end
121
+
122
+ def sln_project_files
123
+ return [] unless sln_file
124
+
125
+ @sln_project_files ||=
126
+ begin
127
+ paths = SlnProjectPathsFinder.
128
+ new(sln_file: sln_file).
129
+ project_paths
130
+
131
+ paths.map do |path|
132
+ fetch_file_from_host(path)
133
+ rescue Dependabot::DependencyFileNotFound
134
+ # Don't worry about missing files too much for now (at least
135
+ # until we start resolving properties)
136
+ nil
137
+ end.compact
138
+ end
139
+ end
140
+
141
+ def csproj_file
142
+ @csproj_file ||=
143
+ begin
144
+ file = repo_contents.find { |f| f.name.end_with?(".csproj") }
145
+ fetch_file_from_host(file.name) if file
146
+ end
147
+ end
148
+
149
+ def vbproj_file
150
+ @vbproj_file ||=
151
+ begin
152
+ file = repo_contents.find { |f| f.name.end_with?(".vbproj") }
153
+ fetch_file_from_host(file.name) if file
154
+ end
155
+ end
156
+
157
+ def fsproj_file
158
+ @fsproj_file ||=
159
+ begin
160
+ file = repo_contents.find { |f| f.name.end_with?(".fsproj") }
161
+ fetch_file_from_host(file.name) if file
162
+ end
163
+ end
164
+
165
+ def nuget_config
166
+ @nuget_config ||=
167
+ begin
168
+ file = repo_contents.
169
+ find { |f| f.name.casecmp("nuget.config").zero? }
170
+ file = fetch_file_from_host(file.name) if file
171
+ file&.tap { |f| f.support_file = true }
172
+ end
173
+ end
174
+
175
+ def imported_property_files
176
+ imported_property_files = []
177
+
178
+ [*project_files, *directory_build_props_files].each do |proj_file|
179
+ previously_fetched_files = project_files + imported_property_files
180
+ imported_property_files +=
181
+ fetch_imported_property_files(
182
+ file: proj_file,
183
+ previously_fetched_files: previously_fetched_files
184
+ )
185
+ end
186
+
187
+ imported_property_files
188
+ end
189
+
190
+ def fetch_imported_property_files(file:, previously_fetched_files:)
191
+ paths =
192
+ ImportPathsFinder.new(project_file: file).import_paths +
193
+ ImportPathsFinder.new(project_file: file).project_reference_paths
194
+
195
+ paths.flat_map do |path|
196
+ next if previously_fetched_files.map(&:name).include?(path)
197
+ next if file.name == path
198
+ next if path.include?("$(")
199
+
200
+ fetched_file = fetch_file_from_host(path)
201
+ grandchild_property_files = fetch_imported_property_files(
202
+ file: fetched_file,
203
+ previously_fetched_files: previously_fetched_files + [file]
204
+ )
205
+ [fetched_file, *grandchild_property_files]
206
+ rescue Dependabot::DependencyFileNotFound
207
+ # Don't worry about missing files too much for now (at least
208
+ # until we start resolving properties)
209
+ nil
210
+ end.compact
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ Dependabot::FileFetchers.register("nuget", Dependabot::Nuget::FileFetcher)
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require "dependabot/dependency"
6
+ require "dependabot/nuget/file_parser"
7
+
8
+ # For details on packages.config files see:
9
+ # https://docs.microsoft.com/en-us/nuget/reference/packages-config
10
+ module Dependabot
11
+ module Nuget
12
+ class FileParser
13
+ class PackagesConfigParser
14
+ require "dependabot/file_parsers/base/dependency_set"
15
+
16
+ DEPENDENCY_SELECTOR = "packages > package"
17
+
18
+ def initialize(packages_config:)
19
+ @packages_config = packages_config
20
+ end
21
+
22
+ def dependency_set
23
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
24
+
25
+ doc = Nokogiri::XML(packages_config.content)
26
+ doc.remove_namespaces!
27
+ doc.css(DEPENDENCY_SELECTOR).each do |dependency_node|
28
+ dependency_set <<
29
+ Dependency.new(
30
+ name: dependency_name(dependency_node),
31
+ version: dependency_version(dependency_node),
32
+ package_manager: "nuget",
33
+ requirements: [{
34
+ requirement: dependency_version(dependency_node),
35
+ file: packages_config.name,
36
+ groups: [],
37
+ source: nil
38
+ }]
39
+ )
40
+ end
41
+
42
+ dependency_set
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :packages_config
48
+
49
+ def dependency_name(dependency_node)
50
+ dependency_node.attribute("id")&.value&.strip ||
51
+ dependency_node.at_xpath("./id")&.content&.strip
52
+ end
53
+
54
+ def dependency_version(dependency_node)
55
+ # Ranges and wildcards aren't allowed in a packages.config - the
56
+ # specified requirement is always an exact version.
57
+ dependency_node.attribute("version")&.value&.strip ||
58
+ dependency_node.at_xpath("./version")&.content&.strip
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require "dependabot/dependency"
6
+ require "dependabot/nuget/file_parser"
7
+
8
+ # For details on how dotnet handles version constraints, see:
9
+ # https://docs.microsoft.com/en-us/nuget/reference/package-versioning
10
+ module Dependabot
11
+ module Nuget
12
+ class FileParser
13
+ class ProjectFileParser
14
+ require "dependabot/file_parsers/base/dependency_set"
15
+ require_relative "property_value_finder"
16
+
17
+ DEPENDENCY_SELECTOR = "ItemGroup > PackageReference, "\
18
+ "ItemGroup > Dependency, "\
19
+ "ItemGroup > DevelopmentDependency"
20
+
21
+ PROPERTY_REGEX = /\$\((?<property>.*?)\)/.freeze
22
+
23
+ def initialize(dependency_files:)
24
+ @dependency_files = dependency_files
25
+ end
26
+
27
+ def dependency_set(project_file:)
28
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
29
+
30
+ doc = Nokogiri::XML(project_file.content)
31
+ doc.remove_namespaces!
32
+ doc.css(DEPENDENCY_SELECTOR).each do |dependency_node|
33
+ name = dependency_name(dependency_node, project_file)
34
+ req = dependency_requirement(dependency_node, project_file)
35
+ version = dependency_version(dependency_node, project_file)
36
+ prop_name = req_property_name(dependency_node)
37
+
38
+ dependency =
39
+ build_dependency(name, req, version, prop_name, project_file)
40
+ dependency_set << dependency if dependency
41
+ end
42
+
43
+ dependency_set
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :dependency_files
49
+
50
+ def build_dependency(name, req, version, prop_name, project_file)
51
+ return unless name
52
+
53
+ # Exclude any dependencies specified using interpolation
54
+ return if [name, req, version].any? { |s| s&.include?("%(") }
55
+
56
+ requirement = {
57
+ requirement: req,
58
+ file: project_file.name,
59
+ groups: [],
60
+ source: nil
61
+ }
62
+
63
+ if prop_name
64
+ # Get the root property name unless no details could be found,
65
+ # in which case use the top-level name to ease debugging
66
+ root_prop_name = details_for_property(prop_name, project_file)&.
67
+ fetch(:root_property_name) || prop_name
68
+ requirement[:metadata] = { property_name: root_prop_name }
69
+ end
70
+
71
+ Dependency.new(
72
+ name: name,
73
+ version: version,
74
+ package_manager: "nuget",
75
+ requirements: [requirement]
76
+ )
77
+ end
78
+
79
+ def dependency_name(dependency_node, project_file)
80
+ raw_name =
81
+ dependency_node.attribute("Include")&.value&.strip ||
82
+ dependency_node.at_xpath("./Include")&.content&.strip
83
+ return unless raw_name
84
+
85
+ evaluated_value(raw_name, project_file)
86
+ end
87
+
88
+ def dependency_requirement(dependency_node, project_file)
89
+ raw_requirement =
90
+ dependency_node.attribute("Version")&.value&.strip ||
91
+ dependency_node.at_xpath("./Version")&.content&.strip
92
+ return unless raw_requirement
93
+
94
+ evaluated_value(raw_requirement, project_file)
95
+ end
96
+
97
+ def dependency_version(dependency_node, project_file)
98
+ requirement = dependency_requirement(dependency_node, project_file)
99
+ return unless requirement
100
+
101
+ # Remove brackets if present
102
+ version = requirement.gsub(/[\(\)\[\]]/, "").strip
103
+
104
+ # We don't know the version for range requirements or wildcard
105
+ # requirements, so return `nil` for these.
106
+ return if version.include?(",") || version.include?("*") ||
107
+ version == ""
108
+
109
+ version
110
+ end
111
+
112
+ def req_property_name(dependency_node)
113
+ raw_requirement =
114
+ dependency_node.attribute("Version")&.value&.strip ||
115
+ dependency_node.at_xpath("./Version")&.content&.strip
116
+ return unless raw_requirement
117
+
118
+ return unless raw_requirement.match?(PROPERTY_REGEX)
119
+
120
+ raw_requirement.
121
+ match(PROPERTY_REGEX).
122
+ named_captures.fetch("property")
123
+ end
124
+
125
+ def evaluated_value(value, project_file)
126
+ return value unless value.match?(PROPERTY_REGEX)
127
+
128
+ property_name = value.match(PROPERTY_REGEX).
129
+ named_captures.fetch("property")
130
+ property_details = details_for_property(property_name, project_file)
131
+
132
+ # Don't halt parsing for a missing property value until we're
133
+ # confident we're fetching property values correctly
134
+ return value unless property_details&.fetch(:value)
135
+
136
+ value.gsub(PROPERTY_REGEX, property_details&.fetch(:value))
137
+ end
138
+
139
+ def details_for_property(property_name, project_file)
140
+ property_value_finder.
141
+ property_details(
142
+ property_name: property_name,
143
+ callsite_file: project_file
144
+ )
145
+ end
146
+
147
+ def property_value_finder
148
+ @property_value_finder ||=
149
+ PropertyValueFinder.new(dependency_files: dependency_files)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/nuget/file_fetcher/import_paths_finder"
4
+ require "dependabot/nuget/file_parser"
5
+
6
+ # For docs, see:
7
+ # - https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-properties
8
+ # - https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build
9
+ module Dependabot
10
+ module Nuget
11
+ class FileParser
12
+ class PropertyValueFinder
13
+ PROPERTY_REGEX = /\$\((?<property>.*?)\)/.freeze
14
+
15
+ def initialize(dependency_files:)
16
+ @dependency_files = dependency_files
17
+ end
18
+
19
+ def property_details(property_name:, callsite_file:, stack: [])
20
+ stack += [[property_name, callsite_file.name]]
21
+
22
+ node_details = deep_find_prop_node(
23
+ property: property_name,
24
+ file: callsite_file
25
+ )
26
+
27
+ node_details ||=
28
+ find_property_in_directory_build_props(
29
+ property: property_name,
30
+ callsite_file: callsite_file
31
+ )
32
+
33
+ return unless node_details
34
+ return node_details unless node_details[:value] =~ PROPERTY_REGEX
35
+
36
+ check_next_level_of_stack(node_details, stack)
37
+ end
38
+
39
+ def check_next_level_of_stack(node_details, stack)
40
+ property_name = node_details.fetch(:value).
41
+ match(PROPERTY_REGEX).
42
+ named_captures.fetch("property")
43
+ callsite_file = dependency_files.
44
+ find { |f| f.name == node_details.fetch(:file) }
45
+
46
+ if stack.include?([property_name, callsite_file.name])
47
+ raise "Circular reference!"
48
+ end
49
+
50
+ property_details(
51
+ property_name: property_name,
52
+ callsite_file: callsite_file,
53
+ stack: stack
54
+ )
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :dependency_files
60
+
61
+ def deep_find_prop_node(property:, file:)
62
+ doc = Nokogiri::XML(file.content)
63
+ doc.remove_namespaces!
64
+ node = doc.at_xpath(property_xpath(property))
65
+
66
+ # If we found a value for the property, return it
67
+ if node
68
+ return node_details(file: file, node: node, property: property)
69
+ end
70
+
71
+ # Otherwise, we need to look in an imported file
72
+ import_path_finder =
73
+ Nuget::FileFetcher::ImportPathsFinder.
74
+ new(project_file: file)
75
+
76
+ import_paths = [
77
+ *import_path_finder.import_paths,
78
+ *import_path_finder.project_reference_paths
79
+ ]
80
+
81
+ file = import_paths.
82
+ map { |p| dependency_files.find { |f| f.name == p } }.
83
+ compact.
84
+ find { |f| deep_find_prop_node(property: property, file: f) }
85
+
86
+ return unless file
87
+
88
+ deep_find_prop_node(property: property, file: file)
89
+ end
90
+
91
+ def find_property_in_directory_build_props(property:, callsite_file:)
92
+ file = buildfile_for_project(callsite_file)
93
+ return unless file
94
+
95
+ deep_find_prop_node(property: property, file: file)
96
+ end
97
+
98
+ def buildfile_for_project(project_file)
99
+ dir = File.dirname(project_file.name)
100
+
101
+ # Nuget walks up the directory structure looking for a
102
+ # Directory.Build.props file
103
+ possible_paths = dir.split("/").map.with_index do |_, i|
104
+ base = dir.split("/").first(i + 1).join("/")
105
+ Pathname.new(base + "/Directory.Build.props").cleanpath.to_path
106
+ end.reverse + ["Directory.Build.props"]
107
+
108
+ path = possible_paths.uniq.
109
+ find { |p| dependency_files.find { |f| f.name == p } }
110
+
111
+ dependency_files.find { |f| f.name == path }
112
+ end
113
+
114
+ def property_xpath(property_name)
115
+ "/Project/PropertyGroup/#{property_name}"
116
+ end
117
+
118
+ def node_details(file:, node:, property:)
119
+ {
120
+ file: file.name,
121
+ node: node,
122
+ value: node.content.strip,
123
+ root_property_name: property
124
+ }
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end