Skip to content

Commit

Permalink
Implement security update support for Swift (#7638)
Browse files Browse the repository at this point in the history
  • Loading branch information
deivid-rodriguez committed Jul 27, 2023
1 parent 20930c0 commit 8c3b7e2
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 14 deletions.
21 changes: 17 additions & 4 deletions swift/lib/dependabot/swift/file_updater/lockfile_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "dependabot/file_updaters/base"
require "dependabot/shared_helpers"
require "dependabot/logger"

module Dependabot
module Swift
Expand All @@ -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 <dependency_name>"
)
try_lockfile_update(dependency_names)

File.read("Package.resolved")
end
Expand All @@ -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 <dependency_names>"
)
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
Expand Down
73 changes: 67 additions & 6 deletions swift/lib/dependabot/swift/update_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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
)
Expand All @@ -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?
Expand All @@ -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
Expand Down
11 changes: 7 additions & 4 deletions swift/lib/dependabot/swift/update_checker/version_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
90 changes: 90 additions & 0 deletions swift/spec/dependabot/swift/update_checker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Binary file added swift/spec/fixtures/git/upload_packs/vapor
Binary file not shown.

0 comments on commit 8c3b7e2

Please sign in to comment.