dependabot-nuget 0.80.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require "dependabot/dependency"
6
+ require "dependabot/file_parsers"
7
+ require "dependabot/file_parsers/base"
8
+
9
+ # For details on how dotnet handles version constraints, see:
10
+ # https://docs.microsoft.com/en-us/nuget/reference/package-versioning
11
+ module Dependabot
12
+ module Nuget
13
+ class FileParser < Dependabot::FileParsers::Base
14
+ require "dependabot/file_parsers/base/dependency_set"
15
+ require_relative "file_parser/project_file_parser"
16
+ require_relative "file_parser/packages_config_parser"
17
+
18
+ PACKAGE_CONF_DEPENDENCY_SELECTOR = "packages > packages"
19
+
20
+ def parse
21
+ dependency_set = DependencySet.new
22
+ dependency_set += project_file_dependencies
23
+ dependency_set += packages_config_dependencies
24
+ dependency_set.dependencies
25
+ end
26
+
27
+ private
28
+
29
+ def project_file_dependencies
30
+ dependency_set = DependencySet.new
31
+
32
+ (project_files + project_import_files).each do |file|
33
+ parser = project_file_parser
34
+ dependency_set += parser.dependency_set(project_file: file)
35
+ end
36
+
37
+ dependency_set
38
+ end
39
+
40
+ def packages_config_dependencies
41
+ dependency_set = DependencySet.new
42
+
43
+ packages_config_files.each do |file|
44
+ parser = PackagesConfigParser.new(packages_config: file)
45
+ dependency_set += parser.dependency_set
46
+ end
47
+
48
+ dependency_set
49
+ end
50
+
51
+ def project_file_parser
52
+ @project_file_parser ||=
53
+ ProjectFileParser.new(dependency_files: dependency_files)
54
+ end
55
+
56
+ def project_files
57
+ dependency_files.select { |df| df.name.match?(/\.[a-z]{2}proj$/) }
58
+ end
59
+
60
+ def packages_config_files
61
+ dependency_files.select do |f|
62
+ f.name.split("/").last.casecmp("packages.config").zero?
63
+ end
64
+ end
65
+
66
+ def project_import_files
67
+ dependency_files -
68
+ project_files -
69
+ packages_config_files -
70
+ [nuget_config]
71
+ end
72
+
73
+ def nuget_config
74
+ dependency_files.find { |f| f.name.casecmp("nuget.config").zero? }
75
+ end
76
+
77
+ def check_required_files
78
+ return if project_files.any? || packages_config_files.any?
79
+
80
+ raise "No project file or packages.config!"
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ Dependabot::FileParsers.register("nuget", Dependabot::Nuget::FileParser)
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/nuget/file_updater"
5
+
6
+ module Dependabot
7
+ module Nuget
8
+ class FileUpdater
9
+ class PackagesConfigDeclarationFinder
10
+ DECLARATION_REGEX =
11
+ %r{<package [^>]*?/>|
12
+ <package [^>]*?[^/]>.*?</package>}mx.freeze
13
+
14
+ attr_reader :dependency_name, :declaring_requirement,
15
+ :packages_config
16
+
17
+ def initialize(dependency_name:, packages_config:,
18
+ declaring_requirement:)
19
+ @dependency_name = dependency_name
20
+ @packages_config = packages_config
21
+ @declaring_requirement = declaring_requirement
22
+
23
+ if declaring_requirement[:file].split("/").last.
24
+ casecmp("packages.config").zero?
25
+ return
26
+ end
27
+
28
+ raise "Requirement not from packages.config!"
29
+ end
30
+
31
+ def declaration_strings
32
+ @declaration_strings ||= fetch_declaration_strings
33
+ end
34
+
35
+ def declaration_nodes
36
+ declaration_strings.map do |declaration_string|
37
+ Nokogiri::XML(declaration_string)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def fetch_declaration_strings
44
+ deep_find_declarations(packages_config.content).select do |nd|
45
+ node = Nokogiri::XML(nd)
46
+ node.remove_namespaces!
47
+ node = node.at_xpath("/package")
48
+
49
+ node_name = node.attribute("id")&.value&.strip ||
50
+ node.at_xpath("./id")&.content&.strip
51
+ next false unless node_name == dependency_name
52
+
53
+ node_requirement = node.attribute("version")&.value&.strip ||
54
+ node.at_xpath("./version")&.content&.strip
55
+ node_requirement == declaring_requirement.fetch(:requirement)
56
+ end
57
+ end
58
+
59
+ def deep_find_declarations(string)
60
+ string.scan(DECLARATION_REGEX).flat_map do |matching_node|
61
+ [matching_node, *deep_find_declarations(matching_node[0..-2])]
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/nuget/file_updater"
5
+
6
+ module Dependabot
7
+ module Nuget
8
+ class FileUpdater
9
+ class ProjectFileDeclarationFinder
10
+ DECLARATION_REGEX =
11
+ %r{
12
+ <PackageReference [^>]*?/>|
13
+ <PackageReference [^>]*?[^/]>.*?</PackageReference>|
14
+ <Dependency [^>]*?/>|
15
+ <Dependency [^>]*?[^/]>.*?</Dependency>|
16
+ <DevelopmentDependency [^>]*?/>|
17
+ <DevelopmentDependency [^>]*?[^/]>.*?</DevelopmentDependency>
18
+ }mx.freeze
19
+
20
+ attr_reader :dependency_name, :declaring_requirement,
21
+ :dependency_files
22
+
23
+ def initialize(dependency_name:, dependency_files:,
24
+ declaring_requirement:)
25
+ @dependency_name = dependency_name
26
+ @dependency_files = dependency_files
27
+ @declaring_requirement = declaring_requirement
28
+ end
29
+
30
+ def declaration_strings
31
+ @declaration_strings ||= fetch_declaration_strings
32
+ end
33
+
34
+ def declaration_nodes
35
+ declaration_strings.map do |declaration_string|
36
+ Nokogiri::XML(declaration_string)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def fetch_declaration_strings
43
+ deep_find_declarations(declaring_file.content).select do |nd|
44
+ node = Nokogiri::XML(nd)
45
+ node.remove_namespaces!
46
+ node = node.at_xpath("/PackageReference") ||
47
+ node.at_xpath("/Dependency") ||
48
+ node.at_xpath("/DevelopmentDependency")
49
+
50
+ node_name = node.attribute("Include")&.value&.strip ||
51
+ node.at_xpath("./Include")&.content&.strip
52
+ next false unless node_name == dependency_name
53
+
54
+ node_requirement = node.attribute("Version")&.value&.strip ||
55
+ node.at_xpath("./Version")&.content&.strip
56
+ node_requirement == declaring_requirement.fetch(:requirement)
57
+ end
58
+ end
59
+
60
+ def deep_find_declarations(string)
61
+ string.scan(DECLARATION_REGEX).flat_map do |matching_node|
62
+ [matching_node, *deep_find_declarations(matching_node[0..-2])]
63
+ end
64
+ end
65
+
66
+ def declaring_file
67
+ filename = declaring_requirement.fetch(:file)
68
+ declaring_file = dependency_files.find { |f| f.name == filename }
69
+ return declaring_file if declaring_file
70
+
71
+ raise "No file found with name #{filename}!"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require "dependabot/dependency_file"
6
+ require "dependabot/nuget/file_updater"
7
+ require "dependabot/nuget/file_parser/property_value_finder"
8
+
9
+ module Dependabot
10
+ module Nuget
11
+ class FileUpdater
12
+ class PropertyValueUpdater
13
+ def initialize(dependency_files:)
14
+ @dependency_files = dependency_files
15
+ end
16
+
17
+ def update_files_for_property_change(property_name:, updated_value:,
18
+ callsite_file:)
19
+ declaration_details =
20
+ property_value_finder.
21
+ property_details(
22
+ property_name: property_name,
23
+ callsite_file: callsite_file
24
+ )
25
+
26
+ declaration_file = dependency_files.find do |f|
27
+ declaration_details.fetch(:file) == f.name
28
+ end
29
+ node = declaration_details.fetch(:node)
30
+
31
+ updated_content = declaration_file.content.sub(
32
+ %r{<#{Regexp.quote(node.name)}>
33
+ \s*#{Regexp.quote(node.content)}\s*
34
+ </#{Regexp.quote(node.name)}>}xm,
35
+ "<#{node.name}>#{updated_value}</#{node.name}>"
36
+ )
37
+
38
+ files = dependency_files.dup
39
+ files[files.index(declaration_file)] =
40
+ update_file(file: declaration_file, content: updated_content)
41
+ files
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :dependency_files
47
+
48
+ def property_value_finder
49
+ @property_value_finder ||=
50
+ Nuget::FileParser::PropertyValueFinder.
51
+ new(dependency_files: dependency_files)
52
+ end
53
+
54
+ def update_file(file:, content:)
55
+ updated_file = file.dup
56
+ updated_file.content = content
57
+ updated_file
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/file_updaters"
4
+ require "dependabot/file_updaters/base"
5
+
6
+ module Dependabot
7
+ module Nuget
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
+ require_relative "file_updater/packages_config_declaration_finder"
10
+ require_relative "file_updater/project_file_declaration_finder"
11
+ require_relative "file_updater/property_value_updater"
12
+
13
+ def self.updated_files_regex
14
+ [
15
+ %r{^[^/]*\.[a-z]{2}proj$},
16
+ /^packages\.config$/i
17
+ ]
18
+ end
19
+
20
+ def updated_dependency_files
21
+ updated_files = dependency_files.dup
22
+
23
+ # Loop through each of the changed requirements, applying changes to
24
+ # all files for that change. Note that the logic is different here
25
+ # to other languages because donet has property inheritance across
26
+ # files
27
+ dependencies.each do |dependency|
28
+ updated_files = update_files_for_dependency(
29
+ files: updated_files,
30
+ dependency: dependency
31
+ )
32
+ end
33
+
34
+ updated_files.reject! { |f| dependency_files.include?(f) }
35
+
36
+ raise "No files changed!" if updated_files.none?
37
+
38
+ updated_files
39
+ end
40
+
41
+ private
42
+
43
+ def project_files
44
+ dependency_files.select { |df| df.name.match?(/\.[a-z]{2}proj$/) }
45
+ end
46
+
47
+ def packages_config_files
48
+ dependency_files.select do |f|
49
+ f.name.split("/").last.casecmp("packages.config").zero?
50
+ end
51
+ end
52
+
53
+ def check_required_files
54
+ return if project_files.any? || packages_config_files.any?
55
+
56
+ raise "No project file or packages.config!"
57
+ end
58
+
59
+ def update_files_for_dependency(files:, dependency:)
60
+ # The UpdateChecker ensures the order of requirements is preserved
61
+ # when updating, so we can zip them together in new/old pairs.
62
+ reqs = dependency.requirements.zip(dependency.previous_requirements).
63
+ reject { |new_req, old_req| new_req == old_req }
64
+
65
+ # Loop through each changed requirement and update the files
66
+ reqs.each do |new_req, old_req|
67
+ raise "Bad req match" unless new_req[:file] == old_req[:file]
68
+ next if new_req[:requirement] == old_req[:requirement]
69
+
70
+ file = files.find { |f| f.name == new_req.fetch(:file) }
71
+
72
+ files =
73
+ if new_req.dig(:metadata, :property_name)
74
+ update_property_value(files, file, new_req)
75
+ else
76
+ update_declaration(files, dependency, file, old_req, new_req)
77
+ end
78
+ end
79
+
80
+ files
81
+ end
82
+
83
+ def update_property_value(files, file, req)
84
+ files = files.dup
85
+ property_name = req.fetch(:metadata).fetch(:property_name)
86
+
87
+ PropertyValueUpdater.
88
+ new(dependency_files: files).
89
+ update_files_for_property_change(
90
+ property_name: property_name,
91
+ updated_value: req.fetch(:requirement),
92
+ callsite_file: file
93
+ )
94
+ end
95
+
96
+ def update_declaration(files, dependency, file, old_req, new_req)
97
+ files = files.dup
98
+
99
+ updated_content = file.content
100
+
101
+ original_declarations(dependency, old_req).each do |old_dec|
102
+ updated_content = updated_content.gsub(
103
+ old_dec,
104
+ updated_declaration(old_dec, old_req, new_req)
105
+ )
106
+ end
107
+
108
+ raise "Expected content to change!" if updated_content == file.content
109
+
110
+ files[files.index(file)] =
111
+ updated_file(file: file, content: updated_content)
112
+ files
113
+ end
114
+
115
+ def original_declarations(dependency, requirement)
116
+ declaration_finder(dependency, requirement).declaration_strings
117
+ end
118
+
119
+ def declaration_finder(dependency, requirement)
120
+ @declaration_finders ||= {}
121
+
122
+ requirement_fn = requirement.fetch(:file)
123
+ @declaration_finders[dependency.hash + requirement.hash] ||=
124
+ if requirement_fn.split("/").last.casecmp("packages.config").zero?
125
+ PackagesConfigDeclarationFinder.new(
126
+ dependency_name: dependency.name,
127
+ declaring_requirement: requirement,
128
+ packages_config:
129
+ packages_config_files.find { |f| f.name == requirement_fn }
130
+ )
131
+ else
132
+ ProjectFileDeclarationFinder.new(
133
+ dependency_name: dependency.name,
134
+ declaring_requirement: requirement,
135
+ dependency_files: dependency_files
136
+ )
137
+ end
138
+ end
139
+
140
+ def updated_declaration(old_declaration, previous_req, requirement)
141
+ original_req_string = previous_req.fetch(:requirement)
142
+
143
+ old_declaration.gsub(
144
+ original_req_string,
145
+ requirement.fetch(:requirement)
146
+ )
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ Dependabot::FileUpdaters.register("nuget", Dependabot::Nuget::FileUpdater)
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/metadata_finders"
5
+ require "dependabot/metadata_finders/base"
6
+
7
+ module Dependabot
8
+ module Nuget
9
+ class MetadataFinder < Dependabot::MetadataFinders::Base
10
+ private
11
+
12
+ def look_up_source
13
+ return Source.from_url(dependency_source_url) if dependency_source_url
14
+
15
+ look_up_source_in_nuspec(dependency_nuspec_file)
16
+ end
17
+
18
+ def look_up_source_in_nuspec(nuspec)
19
+ potential_source_urls = [
20
+ nuspec.at_css("package > metadata > repository")&.
21
+ attribute("url")&.value,
22
+ nuspec.at_css("package > metadata > repository > url")&.content,
23
+ nuspec.at_css("package > metadata > projectUrl")&.content,
24
+ nuspec.at_css("package > metadata > licenseUrl")&.content
25
+ ].compact
26
+
27
+ source_url = potential_source_urls.find { |url| Source.from_url(url) }
28
+ source_url ||= source_from_anywhere_in_nuspec(nuspec)
29
+
30
+ Source.from_url(source_url)
31
+ end
32
+
33
+ def source_from_anywhere_in_nuspec(nuspec)
34
+ github_urls = []
35
+ nuspec.to_s.scan(Source::SOURCE_REGEX) do
36
+ github_urls << Regexp.last_match.to_s
37
+ end
38
+
39
+ github_urls.find do |url|
40
+ repo = Source.from_url(url).repo
41
+ repo.downcase.end_with?(dependency.name.downcase)
42
+ end
43
+ end
44
+
45
+ def dependency_nuspec_file
46
+ return @dependency_nuspec_file unless @dependency_nuspec_file.nil?
47
+
48
+ response = Excon.get(
49
+ dependency_nuspec_url,
50
+ headers: auth_header,
51
+ idempotent: true,
52
+ **SharedHelpers.excon_defaults
53
+ )
54
+
55
+ @dependency_nuspec_file = Nokogiri::XML(response.body)
56
+ end
57
+
58
+ # rubocop:disable Metrics/AbcSize
59
+ def dependency_nuspec_url
60
+ source = dependency.requirements.
61
+ find { |r| r&.fetch(:source) }&.fetch(:source)
62
+
63
+ if source&.key?(:nuspec_url)
64
+ source.fetch(:nuspec_url) ||
65
+ "https://api.nuget.org/v3-flatcontainer/"\
66
+ "#{dependency.name.downcase}/#{dependency.version}/"\
67
+ "#{dependency.name.downcase}.nuspec"
68
+ elsif source&.key?(:nuspec_url)
69
+ source.fetch("nuspec_url") ||
70
+ "https://api.nuget.org/v3-flatcontainer/"\
71
+ "#{dependency.name.downcase}/#{dependency.version}/"\
72
+ "#{dependency.name.downcase}.nuspec"
73
+ else
74
+ "https://api.nuget.org/v3-flatcontainer/"\
75
+ "#{dependency.name.downcase}/#{dependency.version}/"\
76
+ "#{dependency.name.downcase}.nuspec"
77
+ end
78
+ end
79
+ # rubocop:enable Metrics/AbcSize
80
+
81
+ def dependency_source_url
82
+ source = dependency.requirements.
83
+ find { |r| r&.fetch(:source) }&.fetch(:source)
84
+
85
+ return unless source
86
+ return source.fetch(:source_url) if source.key?(:source_url)
87
+
88
+ source.fetch("source_url")
89
+ end
90
+
91
+ def auth_header
92
+ source = dependency.requirements.
93
+ find { |r| r&.fetch(:source) }&.fetch(:source)
94
+ url = source&.fetch(:url, nil) || source&.fetch("url")
95
+
96
+ token = credentials.
97
+ select { |cred| cred["type"] == "nuget_feed" }.
98
+ find { |cred| cred["url"] == url }&.
99
+ fetch("token", nil)
100
+
101
+ return {} unless token
102
+
103
+ if token.include?(":")
104
+ encoded_token = Base64.encode64(token).delete("\n")
105
+ { "Authorization" => "Basic #{encoded_token}" }
106
+ elsif Base64.decode64(token).ascii_only? &&
107
+ Base64.decode64(token).include?(":")
108
+ { "Authorization" => "Basic #{token.delete("\n")}" }
109
+ else
110
+ { "Authorization" => "Bearer #{token}" }
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ Dependabot::MetadataFinders.register("nuget", Dependabot::Nuget::MetadataFinder)
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/utils"
4
+ require "dependabot/nuget/version"
5
+
6
+ # For details on .NET version constraints see:
7
+ # https://docs.microsoft.com/en-us/nuget/reference/package-versioning
8
+ module Dependabot
9
+ module Nuget
10
+ class Requirement < Gem::Requirement
11
+ def self.parse(obj)
12
+ return ["=", Nuget::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
13
+
14
+ unless (matches = PATTERN.match(obj.to_s))
15
+ msg = "Illformed requirement [#{obj.inspect}]"
16
+ raise BadRequirementError, msg
17
+ end
18
+
19
+ return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
20
+
21
+ [matches[1] || "=", Nuget::Version.new(matches[2])]
22
+ end
23
+
24
+ # For consistency with other langauges, we define a requirements array.
25
+ # Dotnet doesn't have an `OR` separator for requirements, so it always
26
+ # contains a single element.
27
+ def self.requirements_array(requirement_string)
28
+ [new(requirement_string)]
29
+ end
30
+
31
+ def initialize(*requirements)
32
+ requirements = requirements.flatten.flat_map do |req_string|
33
+ convert_dotnet_constraint_to_ruby_constraint(req_string)
34
+ end
35
+
36
+ super(requirements)
37
+ end
38
+
39
+ def satisfied_by?(version)
40
+ version = Nuget::Version.new(version.to_s)
41
+ super
42
+ end
43
+
44
+ private
45
+
46
+ def convert_dotnet_constraint_to_ruby_constraint(req_string)
47
+ return unless req_string
48
+
49
+ if req_string&.start_with?("(", "[")
50
+ return convert_dotnet_range_to_ruby_range(req_string)
51
+ end
52
+
53
+ return req_string.split(",").map(&:strip) if req_string.include?(",")
54
+ return req_string unless req_string.include?("*")
55
+
56
+ convert_wildcard_req(req_string)
57
+ end
58
+
59
+ def convert_dotnet_range_to_ruby_range(req_string)
60
+ lower_b, upper_b = req_string.split(",").map(&:strip)
61
+
62
+ lower_b =
63
+ if ["(", "["].include?(lower_b) then nil
64
+ elsif lower_b.start_with?("(") then "> #{lower_b.sub(/\(\s*/, '')}"
65
+ else ">= #{lower_b.sub(/\[\s*/, '').strip}"
66
+ end
67
+
68
+ upper_b =
69
+ if [")", "]"].include?(upper_b) then nil
70
+ elsif upper_b.end_with?(")") then "< #{upper_b.sub(/\s*\)/, '')}"
71
+ else "<= #{upper_b.sub(/\s*\]/, '').strip}"
72
+ end
73
+
74
+ [lower_b, upper_b].compact
75
+ end
76
+
77
+ def convert_wildcard_req(req_string)
78
+ return ">= 0" if req_string.start_with?("*")
79
+
80
+ defined_part = req_string.split("*").first
81
+ suffix = defined_part.end_with?(".") ? "0" : "a"
82
+ version = defined_part + suffix
83
+ "~> #{version}"
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ Dependabot::Utils.
90
+ register_requirement_class("nuget", Dependabot::Nuget::Requirement)