mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
396 lines
12 KiB
Ruby
396 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "cask/blacklist"
|
|
require "cask/checkable"
|
|
require "cask/download"
|
|
require "digest"
|
|
require "utils/curl"
|
|
require "utils/git"
|
|
require "utils/notability"
|
|
|
|
module Cask
|
|
class Audit
|
|
include Checkable
|
|
extend Predicable
|
|
|
|
attr_reader :cask, :commit_range, :download
|
|
|
|
attr_predicate :appcast?
|
|
|
|
def initialize(cask, appcast: false, download: false,
|
|
token_conflicts: false, online: false, strict: false,
|
|
new_cask: false, commit_range: nil, command: SystemCommand)
|
|
@cask = cask
|
|
@appcast = appcast
|
|
@download = download
|
|
@online = online
|
|
@strict = strict
|
|
@new_cask = new_cask
|
|
@commit_range = commit_range
|
|
@token_conflicts = token_conflicts
|
|
@command = command
|
|
end
|
|
|
|
def run!
|
|
check_blacklist
|
|
check_required_stanzas
|
|
check_version
|
|
check_sha256
|
|
check_url
|
|
check_generic_artifacts
|
|
check_token_conflicts
|
|
check_download
|
|
check_https_availability
|
|
check_single_pre_postflight
|
|
check_single_uninstall_zap
|
|
check_untrusted_pkg
|
|
check_hosting_with_appcast
|
|
check_latest_with_appcast
|
|
check_latest_with_auto_updates
|
|
check_stanza_requires_uninstall
|
|
check_appcast_contains_version
|
|
check_github_repository
|
|
check_gitlab_repository
|
|
check_bitbucket_repository
|
|
self
|
|
rescue => e
|
|
odebug "#{e.message}\n#{e.backtrace.join("\n")}"
|
|
add_error "exception while auditing #{cask}: #{e.message}"
|
|
self
|
|
end
|
|
|
|
def success?
|
|
!(errors? || warnings?)
|
|
end
|
|
|
|
def summary_header
|
|
"audit for #{cask}"
|
|
end
|
|
|
|
private
|
|
|
|
def check_untrusted_pkg
|
|
odebug "Auditing pkg stanza: allow_untrusted"
|
|
|
|
return if @cask.sourcefile_path.nil?
|
|
|
|
tap = @cask.tap
|
|
return if tap.nil?
|
|
return if tap.user != "Homebrew"
|
|
|
|
return unless cask.artifacts.any? { |k| k.is_a?(Artifact::Pkg) && k.stanza_options.key?(:allow_untrusted) }
|
|
|
|
add_warning "allow_untrusted is not permitted in official Homebrew Cask taps"
|
|
end
|
|
|
|
def check_stanza_requires_uninstall
|
|
odebug "Auditing stanzas which require an uninstall"
|
|
|
|
return if cask.artifacts.none? { |k| k.is_a?(Artifact::Pkg) || k.is_a?(Artifact::Installer) }
|
|
return if cask.artifacts.any? { |k| k.is_a?(Artifact::Uninstall) }
|
|
|
|
add_warning "installer and pkg stanzas require an uninstall stanza"
|
|
end
|
|
|
|
def check_single_pre_postflight
|
|
odebug "Auditing preflight and postflight stanzas"
|
|
|
|
if cask.artifacts.count { |k| k.is_a?(Artifact::PreflightBlock) && k.directives.key?(:preflight) } > 1
|
|
add_warning "only a single preflight stanza is allowed"
|
|
end
|
|
|
|
count = cask.artifacts.count do |k|
|
|
k.is_a?(Artifact::PostflightBlock) &&
|
|
k.directives.key?(:postflight)
|
|
end
|
|
return unless count > 1
|
|
|
|
add_warning "only a single postflight stanza is allowed"
|
|
end
|
|
|
|
def check_single_uninstall_zap
|
|
odebug "Auditing single uninstall_* and zap stanzas"
|
|
|
|
if cask.artifacts.count { |k| k.is_a?(Artifact::Uninstall) } > 1
|
|
add_warning "only a single uninstall stanza is allowed"
|
|
end
|
|
|
|
count = cask.artifacts.count do |k|
|
|
k.is_a?(Artifact::PreflightBlock) &&
|
|
k.directives.key?(:uninstall_preflight)
|
|
end
|
|
|
|
add_warning "only a single uninstall_preflight stanza is allowed" if count > 1
|
|
|
|
count = cask.artifacts.count do |k|
|
|
k.is_a?(Artifact::PostflightBlock) &&
|
|
k.directives.key?(:uninstall_postflight)
|
|
end
|
|
|
|
add_warning "only a single uninstall_postflight stanza is allowed" if count > 1
|
|
|
|
return unless cask.artifacts.count { |k| k.is_a?(Artifact::Zap) } > 1
|
|
|
|
add_warning "only a single zap stanza is allowed"
|
|
end
|
|
|
|
def check_required_stanzas
|
|
odebug "Auditing required stanzas"
|
|
[:version, :sha256, :url, :homepage].each do |sym|
|
|
add_error "a #{sym} stanza is required" unless cask.send(sym)
|
|
end
|
|
add_error "at least one name stanza is required" if cask.name.empty?
|
|
# TODO: specific DSL knowledge should not be spread around in various files like this
|
|
installable_artifacts = cask.artifacts.reject { |k| [:uninstall, :zap].include?(k) }
|
|
add_error "at least one activatable artifact stanza is required" if installable_artifacts.empty?
|
|
end
|
|
|
|
def check_version
|
|
return unless cask.version
|
|
|
|
check_no_string_version_latest
|
|
check_no_file_separator_in_version
|
|
end
|
|
|
|
def check_no_string_version_latest
|
|
odebug "Verifying version :latest does not appear as a string ('latest')"
|
|
return unless cask.version.raw_version == "latest"
|
|
|
|
add_error "you should use version :latest instead of version 'latest'"
|
|
end
|
|
|
|
def check_no_file_separator_in_version
|
|
odebug "Verifying version does not contain '#{File::SEPARATOR}'"
|
|
return unless cask.version.raw_version.is_a?(String)
|
|
return unless cask.version.raw_version.include?(File::SEPARATOR)
|
|
|
|
add_error "version should not contain '#{File::SEPARATOR}'"
|
|
end
|
|
|
|
def check_sha256
|
|
return unless cask.sha256
|
|
|
|
check_sha256_no_check_if_latest
|
|
check_sha256_actually_256
|
|
check_sha256_invalid
|
|
end
|
|
|
|
def check_sha256_no_check_if_latest
|
|
odebug "Verifying sha256 :no_check with version :latest"
|
|
return unless cask.version.latest?
|
|
return if cask.sha256 == :no_check
|
|
|
|
add_error "you should use sha256 :no_check when version is :latest"
|
|
end
|
|
|
|
def check_sha256_actually_256(sha256: cask.sha256, stanza: "sha256")
|
|
odebug "Verifying #{stanza} string is a legal SHA-256 digest"
|
|
return unless sha256.is_a?(String)
|
|
return if sha256.length == 64 && sha256[/^[0-9a-f]+$/i]
|
|
|
|
add_error "#{stanza} string must be of 64 hexadecimal characters"
|
|
end
|
|
|
|
def check_sha256_invalid(sha256: cask.sha256, stanza: "sha256")
|
|
odebug "Verifying #{stanza} is not a known invalid value"
|
|
empty_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
return unless sha256 == empty_sha256
|
|
|
|
add_error "cannot use the sha256 for an empty string in #{stanza}: #{empty_sha256}"
|
|
end
|
|
|
|
def check_latest_with_appcast
|
|
return unless cask.version.latest?
|
|
return unless cask.appcast
|
|
|
|
add_warning "Casks with an appcast should not use version :latest"
|
|
end
|
|
|
|
def check_latest_with_auto_updates
|
|
return unless cask.version.latest?
|
|
return unless cask.auto_updates
|
|
|
|
add_warning "Casks with `version :latest` should not use `auto_updates`"
|
|
end
|
|
|
|
def check_hosting_with_appcast
|
|
return if cask.appcast
|
|
|
|
add_appcast = "please add an appcast. See https://github.com/Homebrew/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/appcast.md"
|
|
|
|
case cask.url.to_s
|
|
when %r{github.com/([^/]+)/([^/]+)/releases/download/(\S+)}
|
|
return if cask.version.latest?
|
|
|
|
add_warning "Download uses GitHub releases, #{add_appcast}"
|
|
when %r{sourceforge.net/(\S+)}
|
|
return if cask.version.latest?
|
|
|
|
add_warning "Download is hosted on SourceForge, #{add_appcast}"
|
|
when %r{dl.devmate.com/(\S+)}
|
|
add_warning "Download is hosted on DevMate, #{add_appcast}"
|
|
when %r{rink.hockeyapp.net/(\S+)}
|
|
add_warning "Download is hosted on HockeyApp, #{add_appcast}"
|
|
end
|
|
end
|
|
|
|
def check_url
|
|
return unless cask.url
|
|
|
|
check_download_url_format
|
|
end
|
|
|
|
def check_download_url_format
|
|
odebug "Auditing URL format"
|
|
if bad_sourceforge_url?
|
|
add_warning "SourceForge URL format incorrect. See https://github.com/Homebrew/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#sourceforgeosdn-urls"
|
|
elsif bad_osdn_url?
|
|
add_warning "OSDN URL format incorrect. See https://github.com/Homebrew/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#sourceforgeosdn-urls"
|
|
end
|
|
end
|
|
|
|
def bad_url_format?(regex, valid_formats_array)
|
|
return false unless cask.url.to_s.match?(regex)
|
|
|
|
valid_formats_array.none? { |format| cask.url.to_s =~ format }
|
|
end
|
|
|
|
def bad_sourceforge_url?
|
|
bad_url_format?(/sourceforge/,
|
|
[
|
|
%r{\Ahttps://sourceforge\.net/projects/[^/]+/files/latest/download\Z},
|
|
%r{\Ahttps://downloads\.sourceforge\.net/(?!(project|sourceforge)/)},
|
|
])
|
|
end
|
|
|
|
def bad_osdn_url?
|
|
bad_url_format?(/osd/, [%r{\Ahttps?://([^/]+.)?dl\.osdn\.jp/}])
|
|
end
|
|
|
|
def check_generic_artifacts
|
|
cask.artifacts.select { |a| a.is_a?(Artifact::Artifact) }.each do |artifact|
|
|
unless artifact.target.absolute?
|
|
add_error "target must be absolute path for #{artifact.class.english_name} #{artifact.source}"
|
|
end
|
|
end
|
|
end
|
|
|
|
def check_token_conflicts
|
|
return unless @token_conflicts
|
|
return unless core_formula_names.include?(cask.token)
|
|
|
|
add_warning "possible duplicate, cask token conflicts with Homebrew core formula: #{core_formula_url}"
|
|
end
|
|
|
|
def core_tap
|
|
@core_tap ||= CoreTap.instance
|
|
end
|
|
|
|
def core_formula_names
|
|
core_tap.formula_names
|
|
end
|
|
|
|
def core_formula_url
|
|
"#{core_tap.default_remote}/blob/master/Formula/#{cask.token}.rb"
|
|
end
|
|
|
|
def check_download
|
|
return unless download && cask.url
|
|
|
|
odebug "Auditing download"
|
|
downloaded_path = download.perform
|
|
Verify.all(cask, downloaded_path)
|
|
rescue => e
|
|
add_error "download not possible: #{e.message}"
|
|
end
|
|
|
|
def check_appcast_contains_version
|
|
return unless appcast?
|
|
return if cask.appcast.to_s.empty?
|
|
return if cask.appcast.configuration == :no_check
|
|
|
|
appcast_stanza = cask.appcast.to_s
|
|
appcast_contents, = curl_output("--compressed", "--user-agent", HOMEBREW_USER_AGENT_FAKE_SAFARI, "--location",
|
|
"--globoff", "--max-time", "5", appcast_stanza)
|
|
version_stanza = cask.version.to_s
|
|
adjusted_version_stanza = if cask.appcast.configuration.blank?
|
|
version_stanza.split(",")[0].split("-")[0].split("_")[0]
|
|
else
|
|
cask.appcast.configuration
|
|
end
|
|
return if appcast_contents.include? adjusted_version_stanza
|
|
|
|
add_warning "appcast at URL '#{appcast_stanza}' does not contain"\
|
|
" the version number '#{adjusted_version_stanza}':\n#{appcast_contents}"
|
|
rescue
|
|
add_error "appcast at URL '#{appcast_stanza}' offline or looping"
|
|
end
|
|
|
|
def check_github_repository
|
|
user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*})
|
|
return if user.nil?
|
|
|
|
odebug "Auditing GitHub repo"
|
|
|
|
error = SharedAudits.github(user, repo)
|
|
add_error error if error
|
|
end
|
|
|
|
def check_gitlab_repository
|
|
user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*})
|
|
return if user.nil?
|
|
|
|
odebug "Auditing GitLab repo"
|
|
|
|
error = SharedAudits.gitlab(user, repo)
|
|
add_error error if error
|
|
end
|
|
|
|
def check_bitbucket_repository
|
|
user, repo = get_repo_data(%r{https?://bitbucket\.org/([^/]+)/([^/]+)/?.*})
|
|
return if user.nil?
|
|
|
|
odebug "Auditing Bitbucket repo"
|
|
|
|
error = SharedAudits.bitbucket(user, repo)
|
|
add_error error if error
|
|
end
|
|
|
|
def get_repo_data(regex)
|
|
return unless @online
|
|
return unless @new_cask
|
|
|
|
_, user, repo = *regex.match(cask.url.to_s)
|
|
_, user, repo = *regex.match(cask.homepage) unless user
|
|
_, user, repo = *regex.match(cask.appcast.to_s) unless user
|
|
return if !user || !repo
|
|
|
|
repo.gsub!(/.git$/, "")
|
|
|
|
[user, repo]
|
|
end
|
|
|
|
def check_blacklist
|
|
return if cask.tap&.user != "Homebrew"
|
|
return unless reason = Blacklist.blacklisted_reason(cask.token)
|
|
|
|
add_error "#{cask.token} is blacklisted: #{reason}"
|
|
end
|
|
|
|
def check_https_availability
|
|
return unless download
|
|
|
|
if !cask.url.blank? && !cask.url.using
|
|
check_url_for_https_availability(cask.url, user_agents: [cask.url.user_agent])
|
|
end
|
|
check_url_for_https_availability(cask.appcast) unless cask.appcast.blank?
|
|
check_url_for_https_availability(cask.homepage, user_agents: [:browser]) unless cask.homepage.blank?
|
|
end
|
|
|
|
def check_url_for_https_availability(url_to_check, user_agents: [:default])
|
|
problem = curl_check_http_content(url_to_check.to_s, user_agents: user_agents)
|
|
add_error problem if problem
|
|
end
|
|
end
|
|
end
|