dependabot-go_modules 0.87.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.
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "dependabot/dependency"
5
+ require "dependabot/file_parsers/base/dependency_set"
6
+ require "dependabot/go_modules/path_converter"
7
+ require "dependabot/errors"
8
+ require "dependabot/file_parsers"
9
+ require "dependabot/file_parsers/base"
10
+
11
+ module Dependabot
12
+ module GoModules
13
+ class FileParser < Dependabot::FileParsers::Base
14
+ GIT_VERSION_REGEX = /^v\d+\.\d+\.\d+-.*-(?<sha>[0-9a-f]{12})$/.freeze
15
+
16
+ def parse
17
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
18
+
19
+ i = 0
20
+ chunks = module_info(go_mod).lines.
21
+ group_by { |line| line == "{\n" ? i += 1 : i }
22
+ deps = chunks.values.map { |chunk| JSON.parse(chunk.join) }
23
+
24
+ deps.each do |dep|
25
+ # The project itself appears in this list as "Main"
26
+ next if dep["Main"]
27
+
28
+ dependency = dependency_from_details(dep)
29
+ dependency_set << dependency if dependency
30
+ end
31
+
32
+ dependency_set.dependencies
33
+ end
34
+
35
+ private
36
+
37
+ def go_mod
38
+ @go_mod ||= get_original_file("go.mod")
39
+ end
40
+
41
+ def check_required_files
42
+ raise "No go.mod!" unless go_mod
43
+ end
44
+
45
+ def dependency_from_details(details)
46
+ source =
47
+ if rev_identifier?(details) then git_source(details)
48
+ else { type: "default", source: details["Path"] }
49
+ end
50
+
51
+ version = details["Version"]&.sub(/^v?/, "")
52
+
53
+ reqs = [{
54
+ requirement: rev_identifier?(details) ? nil : details["Version"],
55
+ file: go_mod.name,
56
+ source: source,
57
+ groups: []
58
+ }]
59
+
60
+ Dependency.new(
61
+ name: details["Path"],
62
+ version: version,
63
+ requirements: details["Indirect"] ? [] : reqs,
64
+ package_manager: "dep"
65
+ )
66
+ end
67
+
68
+ def module_info(go_mod)
69
+ @module_info ||=
70
+ SharedHelpers.in_a_temporary_directory do |path|
71
+ SharedHelpers.with_git_configured(credentials: credentials) do
72
+ File.write("go.mod", go_mod.content)
73
+
74
+ command = "GO111MODULE=on go mod edit -print > /dev/null"
75
+ command += " && GO111MODULE=on go list -m -json all"
76
+ stdout, stderr, status = Open3.capture3(command)
77
+ handle_parser_error(path, stderr) unless status.success?
78
+ stdout
79
+ end
80
+ end
81
+ end
82
+
83
+ def handle_parser_error(path, stderr)
84
+ case stderr
85
+ when /go: .*: unknown revision/
86
+ line = stderr.lines.grep(/unknown revision/).first
87
+ raise Dependabot::DependencyFileNotResolvable, line.strip
88
+ when /go: .*: unrecognized import path/
89
+ line = stderr.lines.grep(/unrecognized import/).first
90
+ raise Dependabot::DependencyFileNotResolvable, line.strip
91
+ when /go: errors parsing go.mod/
92
+ msg = stderr.gsub(path.to_s, "").strip
93
+ raise Dependabot::DependencyFileNotParseable.new(go_mod.path, msg)
94
+ else
95
+ msg = stderr.gsub(path.to_s, "").strip
96
+ raise Dependabot::DependencyFileNotParseable.new(go_mod.path, msg)
97
+ end
98
+ end
99
+
100
+ def rev_identifier?(dep)
101
+ dep["Version"]&.match?(GIT_VERSION_REGEX)
102
+ end
103
+
104
+ def git_source(dep)
105
+ url = PathConverter.git_url_for_path(dep["Path"])
106
+
107
+ # Currently, we have no way of knowing whether the commit tagged
108
+ # is being used because a branch is being followed or because a
109
+ # particular ref is in use. We *assume* that a particular ref is in
110
+ # use (which means we'll only propose updates when its included in
111
+ # a release)
112
+ {
113
+ type: "git",
114
+ url: url || dep["Path"],
115
+ ref: git_revision(dep),
116
+ branch: nil
117
+ }
118
+ end
119
+
120
+ def git_revision(dep)
121
+ raw_version = dep.fetch("Version")
122
+ return raw_version unless raw_version.match?(GIT_VERSION_REGEX)
123
+
124
+ raw_version.match(GIT_VERSION_REGEX).named_captures.fetch("sha")
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ Dependabot::FileParsers.
131
+ register("go_modules", Dependabot::GoModules::FileParser)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/shared_helpers"
4
+ require "dependabot/file_updaters"
5
+ require "dependabot/file_updaters/base"
6
+
7
+ module Dependabot
8
+ module GoModules
9
+ class FileUpdater < Dependabot::FileUpdaters::Base
10
+ require_relative "file_updater/go_mod_updater"
11
+
12
+ def self.updated_files_regex
13
+ [
14
+ /^go\.mod$/,
15
+ /^go\.sum$/
16
+ ]
17
+ end
18
+
19
+ def updated_dependency_files
20
+ updated_files = []
21
+
22
+ if go_mod && file_changed?(go_mod)
23
+ updated_files <<
24
+ updated_file(
25
+ file: go_mod,
26
+ content: file_updater.updated_go_mod_content
27
+ )
28
+
29
+ if go_sum && go_sum.content != file_updater.updated_go_sum_content
30
+ updated_files <<
31
+ updated_file(
32
+ file: go_sum,
33
+ content: file_updater.updated_go_sum_content
34
+ )
35
+ end
36
+ end
37
+
38
+ raise "No files changed!" if updated_files.none?
39
+
40
+ updated_files
41
+ end
42
+
43
+ private
44
+
45
+ def check_required_files
46
+ return if go_mod
47
+
48
+ raise "No go.mod!"
49
+ end
50
+
51
+ def go_mod
52
+ @go_mod ||= get_original_file("go.mod")
53
+ end
54
+
55
+ def go_sum
56
+ @go_sum ||= get_original_file("go.sum")
57
+ end
58
+
59
+ def file_updater
60
+ @file_updater ||=
61
+ GoModUpdater.new(
62
+ dependencies: dependencies,
63
+ go_mod: go_mod,
64
+ go_sum: go_sum,
65
+ credentials: credentials
66
+ )
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ Dependabot::FileUpdaters.
73
+ register("go_modules", Dependabot::GoModules::FileUpdater)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/utils/go/shared_helper"
4
+ require "dependabot/go_modules/file_updater"
5
+ require "dependabot/go_modules/native_helpers"
6
+
7
+ module Dependabot
8
+ module GoModules
9
+ class FileUpdater
10
+ class GoModUpdater
11
+ def initialize(dependencies:, go_mod:, go_sum:, credentials:)
12
+ @dependencies = dependencies
13
+ @go_mod = go_mod
14
+ @go_sum = go_sum
15
+ @credentials = credentials
16
+ end
17
+
18
+ def updated_go_mod_content
19
+ @updated_go_mod_content ||=
20
+ SharedHelpers.in_a_temporary_directory do
21
+ SharedHelpers.with_git_configured(credentials: credentials) do
22
+ File.write("go.mod", go_mod.content)
23
+
24
+ deps = dependencies.map do |dep|
25
+ {
26
+ name: dep.name,
27
+ version: "v" + dep.version.sub(/^v/i, ""),
28
+ indirect: dep.requirements.empty?
29
+ }
30
+ end
31
+
32
+ SharedHelpers.run_helper_subprocess(
33
+ command: "GO111MODULE=on #{NativeHelpers.helper_path}",
34
+ function: "updateDependencyFile",
35
+ args: { dependencies: deps }
36
+ )
37
+ end
38
+ end
39
+ end
40
+
41
+ def updated_go_sum_content
42
+ return nil unless go_sum
43
+
44
+ # This needs to be run separately so we don't nest subprocess calls
45
+ updated_go_mod_content
46
+
47
+ @updated_go_sum_content ||=
48
+ SharedHelpers.in_a_temporary_directory do
49
+ SharedHelpers.with_git_configured(credentials: credentials) do
50
+ File.write("go.mod", updated_go_mod_content)
51
+ File.write("go.sum", go_sum.content)
52
+ File.write("main.go", dummy_main_go)
53
+
54
+ `GO111MODULE=on go get -d`
55
+ unless $CHILD_STATUS.success?
56
+ raise Dependabot::DependencyFileNotParseable, go_sum.path
57
+ end
58
+
59
+ File.read("go.sum")
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def dummy_main_go
67
+ lines = ["package main", "import ("]
68
+ dependencies.each do |dep|
69
+ lines << "_ \"#{dep.name}\""
70
+ end
71
+ lines << ")"
72
+ lines << "func main() {}"
73
+ lines.join("\n")
74
+ end
75
+
76
+ attr_reader :dependencies, :go_mod, :go_sum, :credentials
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/metadata_finders"
4
+ require "dependabot/metadata_finders/base"
5
+ require "dependabot/go_modules/path_converter"
6
+
7
+ module Dependabot
8
+ module GoModules
9
+ class MetadataFinder < Dependabot::MetadataFinders::Base
10
+ private
11
+
12
+ def look_up_source
13
+ return look_up_git_dependency_source if git_dependency?
14
+
15
+ path_str = (specified_source_string || dependency.name)
16
+ url = Dependabot::GoModules::PathConverter.
17
+ git_url_for_path_without_go_helper(path_str)
18
+ Source.from_url(url) if url
19
+ end
20
+
21
+ def git_dependency?
22
+ return false unless declared_source_details
23
+
24
+ dependency_type =
25
+ declared_source_details.fetch(:type, nil) ||
26
+ declared_source_details.fetch("type")
27
+
28
+ dependency_type == "git"
29
+ end
30
+
31
+ def look_up_git_dependency_source
32
+ specified_url =
33
+ declared_source_details.fetch(:url, nil) ||
34
+ declared_source_details.fetch("url")
35
+
36
+ Source.from_url(specified_url)
37
+ end
38
+
39
+ def specified_source_string
40
+ declared_source_details&.fetch(:source, nil) ||
41
+ declared_source_details&.fetch("source", nil)
42
+ end
43
+
44
+ def declared_source_details
45
+ sources = dependency.requirements.
46
+ map { |r| r.fetch(:source) }.
47
+ uniq.compact
48
+
49
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
50
+
51
+ sources.first
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ Dependabot::MetadataFinders.
58
+ register("go_modules", Dependabot::GoModules::MetadataFinder)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dependabot
4
+ module GoModules
5
+ module NativeHelpers
6
+ def self.helper_path
7
+ clean_path(File.join(native_helpers_root, "go_modules/bin/helper"))
8
+ end
9
+
10
+ def self.native_helpers_root
11
+ default_path = File.join(__dir__, "../../../helpers/install-dir")
12
+ ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
13
+ end
14
+
15
+ def self.clean_path(path)
16
+ Pathname.new(path).cleanpath.to_path
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "nokogiri"
5
+
6
+ require "dependabot/shared_helpers"
7
+ require "dependabot/source"
8
+ require "dependabot/go_modules/native_helpers"
9
+
10
+ module Dependabot
11
+ module GoModules
12
+ module PathConverter
13
+ def self.git_url_for_path(path)
14
+ # Save a query by manually converting golang.org/x names
15
+ import_path = path.gsub(%r{^golang\.org/x}, "github.com/golang")
16
+
17
+ SharedHelpers.run_helper_subprocess(
18
+ command: NativeHelpers.helper_path,
19
+ function: "getVcsRemoteForImport",
20
+ args: { import: import_path }
21
+ )
22
+ end
23
+
24
+ # Used in dependabot-backend, which doesn't have access to any Go
25
+ # helpers.
26
+ # TODO: remove the need for this.
27
+ def self.git_url_for_path_without_go_helper(path)
28
+ # Save a query by manually converting golang.org/x names
29
+ tmp_path = path.gsub(%r{^golang\.org/x}, "github.com/golang")
30
+
31
+ # Currently, Dependabot::Source.new will return `nil` if it can't
32
+ # find a git SCH associated with a path. If it is ever extended to
33
+ # handle non-git sources we'll need to add an additional check here.
34
+ return Source.from_url(tmp_path).url if Source.from_url(tmp_path)
35
+ return "https://#{tmp_path}" if tmp_path.end_with?(".git")
36
+ return unless (metadata_response = fetch_path_metadata(path))
37
+
38
+ # Look for a GitHub, Bitbucket or GitLab URL in the response
39
+ metadata_response.scan(Dependabot::Source::SOURCE_REGEX) do
40
+ source_url = Regexp.last_match.to_s
41
+ return Source.from_url(source_url).url
42
+ end
43
+
44
+ # If none are found, parse the response and return the go-import path
45
+ doc = Nokogiri::XML(metadata_response)
46
+ doc.remove_namespaces!
47
+ import_details =
48
+ doc.xpath("//meta").
49
+ find { |n| n.attributes["name"]&.value == "go-import" }&.
50
+ attributes&.fetch("content")&.value&.split(/\s+/)
51
+ return unless import_details && import_details[1] == "git"
52
+
53
+ import_details[2]
54
+ end
55
+
56
+ def self.fetch_path_metadata(path)
57
+ # TODO: This is not robust! Instead, we should shell out to Go and
58
+ # use https://github.com/Masterminds/vcs.
59
+ response = Excon.get(
60
+ "https://#{path}?go-get=1",
61
+ idempotent: true,
62
+ **SharedHelpers.excon_defaults
63
+ )
64
+
65
+ return unless response.status == 200
66
+
67
+ response.body
68
+ end
69
+ private_class_method :fetch_path_metadata
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ ################################################################################
4
+ # For more details on Go version constraints, see: #
5
+ # - https://github.com/Masterminds/semver #
6
+ # - https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md #
7
+ ################################################################################
8
+
9
+ require "dependabot/go_modules/version"
10
+
11
+ module Dependabot
12
+ module GoModules
13
+ class Requirement < Gem::Requirement
14
+ WILDCARD_REGEX = /(?:\.|^)[xX*]/.freeze
15
+ OR_SEPARATOR = /(?<=[a-zA-Z0-9*])\s*\|{2}/.freeze
16
+
17
+ # Override the version pattern to allow a 'v' prefix
18
+ quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|")
19
+ version_pattern = "v?#{Version::VERSION_PATTERN}"
20
+
21
+ PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{version_pattern})\\s*"
22
+ PATTERN = /\A#{PATTERN_RAW}\z/.freeze
23
+
24
+ # Use GoModules::Version rather than Gem::Version to ensure that
25
+ # pre-release versions aren't transformed.
26
+ def self.parse(obj)
27
+ return ["=", Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
28
+
29
+ unless (matches = PATTERN.match(obj.to_s))
30
+ msg = "Illformed requirement [#{obj.inspect}]"
31
+ raise BadRequirementError, msg
32
+ end
33
+
34
+ return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
35
+
36
+ [matches[1] || "=", Version.new(matches[2])]
37
+ end
38
+
39
+ # Returns an array of requirements. At least one requirement from the
40
+ # returned array must be satisfied for a version to be valid.
41
+ def self.requirements_array(requirement_string)
42
+ return [new(nil)] if requirement_string.nil?
43
+
44
+ requirement_string.strip.split(OR_SEPARATOR).map do |req_string|
45
+ new(req_string)
46
+ end
47
+ end
48
+
49
+ def initialize(*requirements)
50
+ requirements = requirements.flatten.flat_map do |req_string|
51
+ req_string.split(",").map do |r|
52
+ convert_go_constraint_to_ruby_constraint(r.strip)
53
+ end
54
+ end
55
+
56
+ super(requirements)
57
+ end
58
+
59
+ private
60
+
61
+ def convert_go_constraint_to_ruby_constraint(req_string)
62
+ req_string = req_string
63
+ req_string = convert_wildcard_characters(req_string)
64
+
65
+ if req_string.match?(WILDCARD_REGEX)
66
+ ruby_range(req_string.gsub(WILDCARD_REGEX, "").gsub(/^[^\d]/, ""))
67
+ elsif req_string.match?(/^~[^>]/) then convert_tilde_req(req_string)
68
+ elsif req_string.include?(" - ") then convert_hyphen_req(req_string)
69
+ elsif req_string.match?(/^[\dv^]/) then convert_caret_req(req_string)
70
+ elsif req_string.match?(/[<=>]/) then req_string
71
+ else ruby_range(req_string)
72
+ end
73
+ end
74
+
75
+ def convert_wildcard_characters(req_string)
76
+ if req_string.match?(/^[\dv^>~]/)
77
+ replace_wildcard_in_lower_bound(req_string)
78
+ elsif req_string.start_with?("<")
79
+ parts = req_string.split(".")
80
+ parts.map.with_index do |part, index|
81
+ next "0" if part.match?(WILDCARD_REGEX)
82
+ next part.to_i + 1 if parts[index + 1]&.match?(WILDCARD_REGEX)
83
+
84
+ part
85
+ end.join(".")
86
+ else
87
+ req_string
88
+ end
89
+ end
90
+
91
+ def replace_wildcard_in_lower_bound(req_string)
92
+ after_wildcard = false
93
+
94
+ if req_string.start_with?("~")
95
+ req_string = req_string.gsub(/(?:(?:\.|^)[xX*])(\.[xX*])+/, "")
96
+ end
97
+
98
+ req_string.split(".").
99
+ map do |part|
100
+ part.split("-").map.with_index do |p, i|
101
+ # Before we hit a wildcard we just return the existing part
102
+ next p unless p.match?(WILDCARD_REGEX) || after_wildcard
103
+
104
+ # On or after a wildcard we replace the version part with zero
105
+ after_wildcard = true
106
+ i.zero? ? "0" : "a"
107
+ end.join("-")
108
+ end.join(".")
109
+ end
110
+
111
+ def convert_tilde_req(req_string)
112
+ version = req_string.gsub(/^~/, "")
113
+ parts = version.split(".")
114
+ parts << "0" if parts.count < 3
115
+ "~> #{parts.join('.')}"
116
+ end
117
+
118
+ def convert_hyphen_req(req_string)
119
+ lower_bound, upper_bound = req_string.split(/\s+-\s+/)
120
+ [">= #{lower_bound}", "<= #{upper_bound}"]
121
+ end
122
+
123
+ def ruby_range(req_string)
124
+ parts = req_string.split(".")
125
+
126
+ # If we have three or more parts then this is an exact match
127
+ return req_string if parts.count >= 3
128
+
129
+ # If we have no parts then the version is completely unlocked
130
+ return ">= 0" if parts.count.zero?
131
+
132
+ # If we have fewer than three parts we do a partial match
133
+ parts << "0"
134
+ "~> #{parts.join('.')}"
135
+ end
136
+
137
+ # Note: Dep's caret notation implementation doesn't distinguish between
138
+ # pre and post-1.0.0 requirements (unlike in JS)
139
+ def convert_caret_req(req_string)
140
+ version = req_string.gsub(/^\^?v?/, "")
141
+ parts = version.split(".")
142
+ upper_bound = [parts.first.to_i + 1, 0, 0, "a"].map(&:to_s).join(".")
143
+
144
+ [">= #{version}", "< #{upper_bound}"]
145
+ end
146
+ end
147
+ end
148
+ end