dependabot-nuget 0.80.0

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