Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add security update support for Github Actions #6071

Merged
merged 2 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions common/lib/dependabot/git_commit_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ def local_tag_for_latest_version
max_local_tag(allowed_version_tags)
end

def local_tags_for_allowed_versions_matching_existing_precision
select_matching_existing_precision(allowed_version_tags).map { |t| to_local_tag(t) }
end

def local_tags_for_allowed_versions
allowed_version_tags.map { |t| to_local_tag(t) }
end

def allowed_version_tags
allowed_versions(local_tags)
end
Expand All @@ -137,13 +145,14 @@ def filter_lower_versions(tags)
end
end

def local_tag_for_pinned_version
ref = dependency_source_details.fetch(:ref)
tags = local_tags.select { |t| t.commit_sha == ref && version_class.correct?(t.name) }.
sort_by { |t| version_class.new(t.name) }
return if tags.empty?
def most_specific_tag_equivalent_to_pinned_ref
commit_sha = head_commit_for_local_branch(dependency_source_details.fetch(:ref))
most_specific_version_tag_for_sha(commit_sha)
end

tags[-1].name
def local_tag_for_pinned_sha
commit_sha = dependency_source_details.fetch(:ref)
most_specific_version_tag_for_sha(commit_sha)
end

def git_repo_reachable?
Expand All @@ -158,10 +167,7 @@ def git_repo_reachable?
attr_reader :dependency, :credentials, :ignored_versions

def max_local_tag_for_current_precision(tags)
current_precision = precision(dependency.version)

# Find the latest version with the same precision as the pinned version.
max_local_tag(tags.select { |tag| precision(scan_version(tag.name)) == current_precision })
max_local_tag(select_matching_existing_precision(tags))
end

def max_local_tag(tags)
Expand All @@ -170,10 +176,25 @@ def max_local_tag(tags)
to_local_tag(max_version_tag)
end

# Find the latest version with the same precision as the pinned version.
def select_matching_existing_precision(tags)
current_precision = precision(dependency.version)

tags.select { |tag| precision(scan_version(tag.name)) == current_precision }
end

def precision(version)
version.split(".").length
end

def most_specific_version_tag_for_sha(commit_sha)
tags = local_tags.select { |t| t.commit_sha == commit_sha && version_class.correct?(t.name) }.
sort_by { |t| version_class.new(t.name) }
return if tags.empty?

tags[-1].name
end

def allowed_versions(local_tags)
tags =
local_tags.
Expand Down
6 changes: 5 additions & 1 deletion common/lib/dependabot/update_checkers/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def vulnerable?
# Can't (currently) detect whether git dependencies are vulnerable
return false if existing_version_is_sha?

security_advisories.any? { |a| a.vulnerable?(current_version) }
active_advisories.any?
end

def ignore_requirements
Expand All @@ -146,6 +146,10 @@ def ignore_requirements

private

def active_advisories
security_advisories.select { |a| a.vulnerable?(current_version) }
end

def latest_version_resolvable_with_full_unlock?
raise NotImplementedError
end
Expand Down
43 changes: 41 additions & 2 deletions common/spec/dependabot/git_commit_checker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1230,8 +1230,8 @@
end
end

describe "#local_tag_for_pinned_version" do
subject { checker.local_tag_for_pinned_version }
describe "#local_tag_for_pinned_sha" do
subject { checker.local_tag_for_pinned_sha }

context "with a git commit pin" do
let(:source) do
Expand Down Expand Up @@ -1289,6 +1289,45 @@
end
end

describe "#most_specific_tag_equivalent_to_pinned_ref" do
subject { checker.most_specific_tag_equivalent_to_pinned_ref }

let(:source) do
{
type: "git",
url: "https://github.com/actions/checkout",
branch: "main",
ref: source_ref
}
end

let(:repo_url) { "https://github.com/actions/checkout.git" }
let(:service_pack_url) { repo_url + "/info/refs?service=git-upload-pack" }
before do
stub_request(:get, service_pack_url).
to_return(
status: 200,
body: fixture("git", "upload_packs", upload_pack_fixture),
headers: {
"content-type" => "application/x-git-upload-pack-advertisement"
}
)
end
let(:upload_pack_fixture) { "actions-checkout-moving-v2" }

context "for a moving major tag" do
let(:source_ref) { "v2" }

it { is_expected.to eq("v2.3.4") }
end

context "for a fixed patch tag" do
let(:source_ref) { "v2.3.4" }

it { is_expected.to eq("v2.3.4") }
end
end

describe "#git_repo_reachable?" do
subject { checker.git_repo_reachable? }

Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def resolve_git_tags(dependency_set)
git_checker = Dependabot::GitCommitChecker.new(dependency: dep, credentials: credentials)
next unless git_checker.pinned_ref_looks_like_commit_sha?

resolved = git_checker.local_tag_for_pinned_version
resolved = git_checker.local_tag_for_pinned_sha
next if resolved.nil? || !version_class.correct?(resolved)

# Build a Dependency with the resolved version, and rely on DependencySet's merge
Expand Down
59 changes: 59 additions & 0 deletions github_actions/lib/dependabot/github_actions/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/errors"
require "dependabot/github_actions/version"
require "dependabot/github_actions/requirement"
Expand All @@ -23,6 +24,15 @@ def latest_resolvable_version_with_no_unlock
dependency.version
end

def lowest_security_fix_version
@lowest_security_fix_version ||= fetch_lowest_security_fix_version
end

def lowest_resolvable_security_fix_version
# Resolvability isn't an issue for GitHub Actions.
lowest_security_fix_version
end

def updated_requirements # rubocop:disable Metrics/PerceivedComplexity
previous = dependency_source_details
updated = updated_source
Expand All @@ -42,6 +52,12 @@ def updated_requirements # rubocop:disable Metrics/PerceivedComplexity

private

def active_advisories
security_advisories.select do |advisory|
advisory.vulnerable?(version_class.new(git_commit_checker.most_specific_tag_equivalent_to_pinned_ref))
end
end

def latest_version_resolvable_with_full_unlock?
# Full unlock checks aren't relevant for GitHub Actions
false
Expand Down Expand Up @@ -82,6 +98,37 @@ def fetch_latest_version_for_git_dependency
nil
end

def fetch_lowest_security_fix_version
# TODO: Support Docker sources
return unless git_dependency?

fetch_lowest_security_fix_version_for_git_dependency
end

def fetch_lowest_security_fix_version_for_git_dependency
lowest_security_fix_version_tag.fetch(:version)
end

def lowest_security_fix_version_tag
@lowest_security_fix_version_tag ||= begin
tags_matching_precision = git_commit_checker.local_tags_for_allowed_versions_matching_existing_precision
lowest_fixed_version = find_lowest_secure_version(tags_matching_precision)
if lowest_fixed_version
lowest_fixed_version
else
tags = git_commit_checker.local_tags_for_allowed_versions
find_lowest_secure_version(tags)
end
end
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 latest_commit_for_pinned_ref
@latest_commit_for_pinned_ref ||= begin
head_commit_for_ref_sha = git_commit_checker.head_commit_for_pinned_ref
Expand Down Expand Up @@ -114,10 +161,22 @@ def latest_version_tag
end
end

def filter_lower_tags(tags_array)
return tags_array unless current_version

tags_array.
select { |tag| tag.fetch(:version) > current_version }
end

def updated_source
# TODO: Support Docker sources
return dependency_source_details unless git_dependency?

if vulnerable? &&
(new_tag = lowest_security_fix_version_tag)
return dependency_source_details.merge(ref: new_tag.fetch(:tag))
end

# Update the git tag if updating a pinned version
if git_commit_checker.pinned_ref_looks_like_version? &&
(new_tag = latest_version_tag) &&
Expand Down
Loading