diff --git a/swift/lib/dependabot/swift/file_updater/lockfile_updater.rb b/swift/lib/dependabot/swift/file_updater/lockfile_updater.rb index b0f45f9c7c2..0734356a58e 100644 --- a/swift/lib/dependabot/swift/file_updater/lockfile_updater.rb +++ b/swift/lib/dependabot/swift/file_updater/lockfile_updater.rb @@ -2,6 +2,7 @@ require "dependabot/file_updaters/base" require "dependabot/shared_helpers" +require "dependabot/logger" module Dependabot module Swift @@ -18,11 +19,10 @@ def updated_lockfile_content SharedHelpers.in_a_temporary_repo_directory(manifest.directory, repo_contents_path) do File.write(manifest.name, manifest.content) + dependency_names = dependencies.map(&:name).join(" ") + SharedHelpers.with_git_configured(credentials: credentials) do - SharedHelpers.run_shell_command( - "swift package update #{dependencies.map(&:name).join(' ')}", - fingerprint: "swift package update " - ) + try_lockfile_update(dependency_names) File.read("Package.resolved") end @@ -31,6 +31,19 @@ def updated_lockfile_content private + def try_lockfile_update(dependency_names) + SharedHelpers.run_shell_command( + "swift package update #{dependency_names}", + fingerprint: "swift package update " + ) + rescue SharedHelpers::HelperSubprocessFailed => e + # This class is not only used for final lockfile updates, but for + # checking resolvability. So resolvability errors here are expected in + # certain situations and will result in `no_update_possible` outcomes. + # That said, since we're swallowing all errors we at least log them to ease debugging. + Dependabot.logger.info("Lockfile failed to be updated due to error:\n#{e.message}") + end + attr_reader :dependencies, :manifest, :repo_contents_path, :credentials end end diff --git a/swift/lib/dependabot/swift/update_checker.rb b/swift/lib/dependabot/swift/update_checker.rb index b3589f95442..740f2218327 100644 --- a/swift/lib/dependabot/swift/update_checker.rb +++ b/swift/lib/dependabot/swift/update_checker.rb @@ -2,6 +2,7 @@ require "dependabot/update_checkers" require "dependabot/update_checkers/base" +require "dependabot/update_checkers/version_filters" require "dependabot/git_commit_checker" require "dependabot/swift/native_requirement" require "dependabot/swift/file_updater/manifest_updater" @@ -24,6 +25,18 @@ def latest_resolvable_version_with_no_unlock raise NotImplementedError end + def lowest_security_fix_version + @lowest_security_fix_version ||= fetch_lowest_security_fix_version + end + + def lowest_resolvable_security_fix_version + raise "Dependency not vulnerable!" unless vulnerable? + + return @lowest_resolvable_security_fix_version if defined?(@lowest_resolvable_security_fix_version) + + @lowest_resolvable_security_fix_version = fetch_lowest_resolvable_security_fix_version + end + def updated_requirements RequirementsUpdater.new( requirements: old_requirements, @@ -43,14 +56,33 @@ def fetch_latest_version latest_version_tag.fetch(:version) end + def fetch_lowest_security_fix_version + return unless git_commit_checker.pinned_ref_looks_like_version? && latest_version_tag + + lowest_security_fix_version_tag.fetch(:version) + end + def fetch_latest_resolvable_version - Version.new(version_resolver.latest_resolvable_version) + latest_resolvable_version = version_resolver_for(unlocked_requirements).latest_resolvable_version + return current_version unless latest_resolvable_version + + Version.new(latest_resolvable_version) + end + + def fetch_lowest_resolvable_security_fix_version + lowest_resolvable_security_fix_version = version_resolver_for( + force_lowest_security_fix_requirements + ).latest_resolvable_version + return unless lowest_resolvable_security_fix_version + + Version.new(lowest_resolvable_security_fix_version) end - def version_resolver + def version_resolver_for(requirements) VersionResolver.new( dependency: dependency, - manifest: prepared_manifest, + manifest: prepare_manifest_for(requirements), + lockfile: lockfile, repo_contents_path: repo_contents_path, credentials: credentials ) @@ -62,19 +94,29 @@ def unlocked_requirements end end - def prepared_manifest + def force_lowest_security_fix_requirements + NativeRequirement.map_requirements(old_requirements) do |_old_requirement| + "\"#{lowest_security_fix_version}\"...\"#{lowest_security_fix_version}\"" + end + end + + def prepare_manifest_for(new_requirements) DependencyFile.new( name: manifest.name, content: FileUpdater::ManifestUpdater.new( manifest.content, old_requirements: old_requirements, - new_requirements: unlocked_requirements + new_requirements: new_requirements ).updated_manifest_content ) end def manifest - dependency_files.find { |file| file.name == "Package.swift" } + @manifest ||= dependency_files.find { |file| file.name == "Package.swift" } + end + + def lockfile + @lockfile ||= dependency_files.find { |file| file.name == "Package.resolved" } end def latest_version_resolvable_with_full_unlock? @@ -99,6 +141,25 @@ def git_commit_checker def latest_version_tag git_commit_checker.local_tag_for_latest_version end + + def lowest_security_fix_version_tag + tags = git_commit_checker.local_tags_for_allowed_versions + find_lowest_secure_version(tags) + end + + def find_lowest_secure_version(tags) + relevant_tags = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(tags, security_advisories) + relevant_tags = filter_lower_tags(relevant_tags) + + relevant_tags.min_by { |tag| tag.fetch(:version) } + end + + def filter_lower_tags(tags_array) + return tags_array unless current_version + + tags_array. + select { |tag| tag.fetch(:version) > current_version } + end end end end diff --git a/swift/lib/dependabot/swift/update_checker/version_resolver.rb b/swift/lib/dependabot/swift/update_checker/version_resolver.rb index 7bde4018e0e..78f2edca6b9 100644 --- a/swift/lib/dependabot/swift/update_checker/version_resolver.rb +++ b/swift/lib/dependabot/swift/update_checker/version_resolver.rb @@ -8,9 +8,10 @@ module Dependabot module Swift class UpdateChecker < Dependabot::UpdateCheckers::Base class VersionResolver - def initialize(dependency:, manifest:, repo_contents_path:, credentials:) + def initialize(dependency:, manifest:, lockfile:, repo_contents_path:, credentials:) @dependency = dependency @manifest = manifest + @lockfile = lockfile @credentials = credentials @repo_contents_path = repo_contents_path end @@ -29,12 +30,14 @@ def fetch_latest_resolvable_version credentials: credentials ).updated_lockfile_content - lockfile = DependencyFile.new( + return if updated_lockfile_content == lockfile.content + + updated_lockfile = DependencyFile.new( name: "Package.resolved", content: updated_lockfile_content ) - dependency_parser(manifest, lockfile).parse.find do |parsed_dep| + dependency_parser(manifest, updated_lockfile).parse.find do |parsed_dep| parsed_dep.name == dependency.name end.version end @@ -47,7 +50,7 @@ def dependency_parser(manifest, lockfile) ) end - attr_reader :dependency, :manifest, :repo_contents_path, :credentials + attr_reader :dependency, :manifest, :lockfile, :repo_contents_path, :credentials end end end diff --git a/swift/spec/dependabot/swift/update_checker_spec.rb b/swift/spec/dependabot/swift/update_checker_spec.rb index 74e9aef5233..81bae080f7e 100644 --- a/swift/spec/dependabot/swift/update_checker_spec.rb +++ b/swift/spec/dependabot/swift/update_checker_spec.rb @@ -129,4 +129,94 @@ it { is_expected.to eq("12.0.1") } end end + + describe "#lowest_security_fix_version" do + subject(:lowest_security_fix_version) { checker.lowest_security_fix_version } + + let(:name) { "nimble" } + let(:url) { "https://github.com/Quick/Nimble" } + let(:upload_pack_fixture) { "nimble" } + + let(:security_advisories) do + [ + Dependabot::SecurityAdvisory.new( + dependency_name: name, + package_manager: "swift", + vulnerable_versions: ["<= 9.2.1"] + ) + ] + end + + before { stub_upload_pack } + + context "when a supported newer version is available" do + it "updates to the least new supported version" do + is_expected.to eq(Dependabot::Swift::Version.new("10.0.0")) + end + end + + context "with ignored versions" do + let(:ignored_versions) { ["= 10.0.0"] } + + it "doesn't return ignored versions" do + is_expected.to eq(Dependabot::Swift::Version.new("11.0.0")) + end + end + end + + describe "#lowest_resolvable_security_fix_version" do + subject(:lowest_resolvable_security_fix_version) { checker.lowest_resolvable_security_fix_version } + + context "when a supported newer version is available, and resolvable" do + let(:name) { "nimble" } + let(:url) { "https://github.com/Quick/Nimble" } + let(:upload_pack_fixture) { "nimble" } + + let(:security_advisories) do + [ + Dependabot::SecurityAdvisory.new( + dependency_name: name, + package_manager: "swift", + vulnerable_versions: ["<= 9.2.1"] + ) + ] + end + + before { stub_upload_pack } + + it "updates to the least new supported version" do + is_expected.to eq(Dependabot::Swift::Version.new("10.0.0")) + end + + context "with ignored versions" do + let(:ignored_versions) { ["= 10.0.0"] } + + it "doesn't return ignored versions" do + is_expected.to eq(Dependabot::Swift::Version.new("11.0.0")) + end + end + end + + context "when fixed version has conflicts with the project" do + let(:project_name) { "conflicts" } + + let(:name) { "vapor" } + let(:url) { "https://github.com/vapor/vapor" } + let(:upload_pack_fixture) { "vapor" } + + let(:security_advisories) do + [ + Dependabot::SecurityAdvisory.new( + dependency_name: name, + package_manager: "swift", + vulnerable_versions: ["<= 4.6.2"] + ) + ] + end + + before { stub_upload_pack } + + it { is_expected.to be_nil } + end + end end diff --git a/swift/spec/fixtures/git/upload_packs/vapor b/swift/spec/fixtures/git/upload_packs/vapor new file mode 100644 index 00000000000..3731b347b1f Binary files /dev/null and b/swift/spec/fixtures/git/upload_packs/vapor differ diff --git a/swift/spec/fixtures/projects/conflicts/Package.resolved b/swift/spec/fixtures/projects/conflicts/Package.resolved new file mode 100644 index 00000000000..3177c8bf797 --- /dev/null +++ b/swift/spec/fixtures/projects/conflicts/Package.resolved @@ -0,0 +1,169 @@ +{ + "object": { + "pins": [ + { + "package": "Console", + "repositoryURL": "https://github.com/vapor/console.git", + "state": { + "branch": null, + "revision": "74cfbea629d4aac34a97cead2447a6870af1950b", + "version": "3.1.1" + } + }, + { + "package": "Core", + "repositoryURL": "https://github.com/vapor/core.git", + "state": { + "branch": null, + "revision": "89c6989fd8b1e08acfd198afba1c38971bb814b2", + "version": "3.10.1" + } + }, + { + "package": "Crypto", + "repositoryURL": "https://github.com/vapor/crypto.git", + "state": { + "branch": null, + "revision": "105c2f875588bf40dd24c00cef3644bf8e327770", + "version": "3.4.1" + } + }, + { + "package": "DatabaseKit", + "repositoryURL": "https://github.com/vapor/database-kit.git", + "state": { + "branch": null, + "revision": "8f352c8e66dab301ab9bfef912a01ce1361ba1e4", + "version": "1.3.3" + } + }, + { + "package": "HTTP", + "repositoryURL": "https://github.com/vapor/http.git", + "state": { + "branch": null, + "revision": "0464b715a4b59f54078bcf7a4b424767b03db5a5", + "version": "3.4.0" + } + }, + { + "package": "Multipart", + "repositoryURL": "https://github.com/vapor/multipart.git", + "state": { + "branch": null, + "revision": "fb216c5a8ef07dcd90aec8a4155e86c831acce97", + "version": "3.1.3" + } + }, + { + "package": "Routing", + "repositoryURL": "https://github.com/vapor/routing.git", + "state": { + "branch": null, + "revision": "d76f339c9716785e5079af9d7075d28ff7da3d92", + "version": "3.1.0" + } + }, + { + "package": "Service", + "repositoryURL": "https://github.com/vapor/service.git", + "state": { + "branch": null, + "revision": "fa5b5de62bd68bcde9a69933f31319e46c7275fb", + "version": "1.0.2" + } + }, + { + "package": "swift-example-vapor3", + "repositoryURL": "https://github.com/dependabot-fixtures/swift-example-vapor3", + "state": { + "branch": null, + "revision": "5f3ac6edfda423354457b34f85af3cc1211e58c3", + "version": "1.0.0" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "546610d52b19be3e19935e0880bb06b9c03f5cef", + "version": "1.14.4" + } + }, + { + "package": "swift-nio-ssl", + "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", + "state": { + "branch": null, + "revision": "0f3999f3e3c359cc74480c292644c3419e44a12f", + "version": "1.4.0" + } + }, + { + "package": "swift-nio-ssl-support", + "repositoryURL": "https://github.com/apple/swift-nio-ssl-support.git", + "state": { + "branch": null, + "revision": "c02eec4e0e6d351cd092938cf44195a8e669f555", + "version": "1.0.0" + } + }, + { + "package": "swift-nio-zlib-support", + "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", + "state": { + "branch": null, + "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version": "1.0.0" + } + }, + { + "package": "TemplateKit", + "repositoryURL": "https://github.com/vapor/template-kit.git", + "state": { + "branch": null, + "revision": "4370aa99c01fc19cc8272b67bf7204b2d2063680", + "version": "1.5.0" + } + }, + { + "package": "URLEncodedForm", + "repositoryURL": "https://github.com/vapor/url-encoded-form.git", + "state": { + "branch": null, + "revision": "20f68fbe7fac006d4d0617ea4edcba033227359e", + "version": "1.1.0" + } + }, + { + "package": "Validation", + "repositoryURL": "https://github.com/vapor/validation.git", + "state": { + "branch": null, + "revision": "4de213cf319b694e4ce19e5339592601d4dd3ff6", + "version": "2.1.1" + } + }, + { + "package": "Vapor", + "repositoryURL": "https://github.com/vapor/vapor.git", + "state": { + "branch": null, + "revision": "642f3d4d1f0eafad651c85524d0d1c698b55399f", + "version": "3.3.3" + } + }, + { + "package": "WebSocket", + "repositoryURL": "https://github.com/vapor/websocket.git", + "state": { + "branch": null, + "revision": "d85e5b6dce4d04065865f77385fc3324f98178f6", + "version": "1.1.2" + } + } + ] + }, + "version": 1 +} diff --git a/swift/spec/fixtures/projects/conflicts/Package.swift b/swift/spec/fixtures/projects/conflicts/Package.swift new file mode 100644 index 00000000000..c433f601471 --- /dev/null +++ b/swift/spec/fixtures/projects/conflicts/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version:5.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. +import PackageDescription + +let package = Package( + name: "Foo", + platforms: [ + .macOS(.v10_13), .iOS(.v11), .tvOS(.v11), .watchOS(.v4) + ], + dependencies: [ + .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), + .package(url: "https://github.com/dependabot-fixtures/swift-example-vapor3", from: "1.0.0"), + ], + swiftLanguageVersions: [.v5] +)