2019-04-19 15:38:03 +09:00
# frozen_string_literal: true
2018-09-03 19:39:07 +01:00
require " cask/checkable "
require " cask/download "
2016-08-18 22:11:42 +03:00
require " digest "
2018-10-10 21:36:02 +00:00
require " utils/curl "
2017-05-07 06:41:40 +02:00
require " utils/git "
2016-08-18 22:11:42 +03:00
2018-09-06 08:29:14 +02:00
module Cask
2016-09-24 13:52:43 +02:00
class Audit
include Checkable
2019-05-07 17:06:54 +02:00
extend Predicable
2016-08-18 22:11:42 +03:00
2017-05-07 06:41:40 +02:00
attr_reader :cask , :commit_range , :download
2016-08-18 22:11:42 +03:00
2019-05-07 17:06:54 +02:00
attr_predicate :check_appcast?
def initialize ( cask , check_appcast : false , download : false , check_token_conflicts : false ,
commit_range : nil , command : SystemCommand )
2016-09-24 13:52:43 +02:00
@cask = cask
2019-05-07 17:06:54 +02:00
@check_appcast = check_appcast
2016-09-24 13:52:43 +02:00
@download = download
2017-05-07 06:41:40 +02:00
@commit_range = commit_range
2016-09-24 13:52:43 +02:00
@check_token_conflicts = check_token_conflicts
@command = command
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_token_conflicts?
@check_token_conflicts
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def run!
check_required_stanzas
check_version
check_sha256
check_url
check_generic_artifacts
check_token_conflicts
check_download
2018-10-10 21:36:02 +00:00
check_https_availability
2017-10-30 20:47:22 -03:00
check_single_pre_postflight
2017-10-27 16:53:22 -03:00
check_single_uninstall_zap
2018-03-25 15:30:16 +10:00
check_untrusted_pkg
2018-06-05 16:42:15 +10:00
check_hosting_with_appcast
2018-03-27 20:56:01 +10:00
check_latest_with_appcast
2018-07-12 16:13:46 +10:00
check_latest_with_auto_updates
2018-05-19 12:38:47 +10:00
check_stanza_requires_uninstall
2019-05-07 17:06:54 +02:00
check_appcast_contains_version
2016-09-24 13:52:43 +02:00
self
2018-09-02 20:14:54 +01:00
rescue = > e
2016-09-24 13:52:43 +02:00
odebug " #{ e . message } \n #{ e . backtrace . join ( " \n " ) } "
add_error " exception while auditing #{ cask } : #{ e . message } "
self
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def success?
! ( errors? || warnings? )
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def summary_header
" audit for #{ cask } "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
private
2016-08-18 22:11:42 +03:00
2018-03-25 15:30:16 +10:00
def check_untrusted_pkg
odebug " Auditing pkg stanza: allow_untrusted "
return if @cask . sourcefile_path . nil?
tap = @cask . tap
2018-05-15 16:52:10 +02:00
return if tap . nil?
2018-05-25 18:03:16 +02:00
return if tap . user != " Homebrew "
2018-03-25 15:30:16 +10:00
2018-09-06 06:47:29 +02:00
return unless cask . artifacts . any? { | k | k . is_a? ( Artifact :: Pkg ) && k . stanza_options . key? ( :allow_untrusted ) }
2018-09-17 02:45:00 +02:00
2018-09-03 20:12:29 +01:00
add_warning " allow_untrusted is not permitted in official Homebrew Cask taps "
2018-03-25 15:30:16 +10:00
end
2018-05-19 12:38:47 +10:00
def check_stanza_requires_uninstall
odebug " Auditing stanzas which require an uninstall "
2018-09-06 06:47:29 +02:00
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 ) }
2018-09-17 02:45:00 +02:00
2018-05-19 12:38:47 +10:00
add_warning " installer and pkg stanzas require an uninstall stanza "
end
2017-10-30 20:47:22 -03:00
def check_single_pre_postflight
odebug " Auditing preflight and postflight stanzas "
2018-09-06 06:47:29 +02:00
if cask . artifacts . count { | k | k . is_a? ( Artifact :: PreflightBlock ) && k . directives . key? ( :preflight ) } > 1
2017-11-01 22:35:41 -03:00
add_warning " only a single preflight stanza is allowed "
end
2017-10-30 20:47:22 -03:00
2018-09-02 16:15:09 +01:00
count = cask . artifacts . count do | k |
2018-09-06 06:47:29 +02:00
k . is_a? ( Artifact :: PostflightBlock ) &&
2018-09-02 16:15:09 +01:00
k . directives . key? ( :postflight )
end
return unless count > 1
2017-11-02 09:56:51 -03:00
add_warning " only a single postflight stanza is allowed "
2017-10-30 20:47:22 -03:00
end
2017-10-27 16:53:22 -03:00
def check_single_uninstall_zap
odebug " Auditing single uninstall_* and zap stanzas "
2018-09-06 06:47:29 +02:00
if cask . artifacts . count { | k | k . is_a? ( Artifact :: Uninstall ) } > 1
2017-11-01 22:35:41 -03:00
add_warning " only a single uninstall stanza is allowed "
end
2017-10-27 16:53:22 -03:00
2018-09-02 16:15:09 +01:00
count = cask . artifacts . count do | k |
2018-09-06 06:47:29 +02:00
k . is_a? ( Artifact :: PreflightBlock ) &&
2018-09-02 16:15:09 +01:00
k . directives . key? ( :uninstall_preflight )
end
2019-02-19 13:11:32 +00:00
add_warning " only a single uninstall_preflight stanza is allowed " if count > 1
2017-10-27 16:53:22 -03:00
2018-09-02 16:15:09 +01:00
count = cask . artifacts . count do | k |
2018-09-06 06:47:29 +02:00
k . is_a? ( Artifact :: PostflightBlock ) &&
2018-09-02 16:15:09 +01:00
k . directives . key? ( :uninstall_postflight )
end
2019-02-19 13:11:32 +00:00
add_warning " only a single uninstall_postflight stanza is allowed " if count > 1
2017-10-27 16:53:22 -03:00
2018-09-06 06:47:29 +02:00
return unless cask . artifacts . count { | k | k . is_a? ( Artifact :: Zap ) } > 1
2018-09-17 02:45:00 +02:00
2017-11-02 09:56:51 -03:00
add_warning " only a single zap stanza is allowed "
2017-10-27 16:53:22 -03:00
end
2016-09-24 13:52:43 +02:00
def check_required_stanzas
odebug " Auditing required stanzas "
2016-10-14 20:17:25 +02:00
[ :version , :sha256 , :url , :homepage ] . each do | sym |
2016-09-24 13:52:43 +02:00
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
2018-07-23 23:04:49 +02:00
installable_artifacts = cask . artifacts . reject { | k | [ :uninstall , :zap ] . include? ( k ) }
2016-09-24 13:52:43 +02:00
add_error " at least one activatable artifact stanza is required " if installable_artifacts . empty?
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def check_version
return unless cask . version
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
check_no_string_version_latest
2016-12-31 21:44:42 +01:00
check_no_file_separator_in_version
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_no_string_version_latest
odebug " Verifying version :latest does not appear as a string ('latest') "
return unless cask . version . raw_version == " latest "
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
add_error " you should use version :latest instead of version 'latest' "
end
2016-08-18 22:11:42 +03:00
2016-12-31 21:44:42 +01:00
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 )
2018-09-17 02:45:00 +02:00
2016-12-31 21:44:42 +01:00
add_error " version should not contain ' #{ File :: SEPARATOR } ' "
end
2016-09-24 13:52:43 +02:00
def check_sha256
return unless cask . sha256
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
check_sha256_no_check_if_latest
check_sha256_actually_256
check_sha256_invalid
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_sha256_no_check_if_latest
odebug " Verifying sha256 :no_check with version :latest "
2018-09-10 19:35:08 +02:00
return unless cask . version . latest?
return if cask . sha256 == :no_check
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
add_error " you should use sha256 :no_check when version is :latest "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
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 )
2016-10-14 20:03:34 +02:00
return if sha256 . length == 64 && sha256 [ / ^[0-9a-f]+$ /i ]
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
add_error " #{ stanza } string must be of 64 hexadecimal characters "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
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
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
add_error " cannot use the sha256 for an empty string in #{ stanza } : #{ empty_sha256 } "
end
2016-08-18 22:11:42 +03:00
2018-03-27 20:56:01 +10:00
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
2018-07-12 16:13:46 +10:00
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
2018-06-05 16:42:15 +10:00
def check_hosting_with_appcast
2018-03-26 21:25:00 +10:00
return if cask . appcast
2018-06-05 16:42:15 +10:00
2018-06-15 17:01:27 +10:00
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+) }
2018-08-07 09:49:16 +10:00
return if cask . version . latest?
2018-09-17 02:45:00 +02:00
2018-06-15 17:01:27 +10:00
add_warning " Download uses GitHub releases, #{ add_appcast } "
when %r{ sourceforge.net/( \ S+) }
return if cask . version . latest?
2018-09-17 02:45:00 +02:00
2018-06-15 17:01:27 +10:00
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
2018-06-05 16:42:15 +10:00
end
2016-09-24 13:52:43 +02:00
def check_url
return unless cask . url
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
check_download_url_format
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_download_url_format
odebug " Auditing URL format "
if bad_sourceforge_url?
2018-05-25 18:03:16 +02:00
add_warning " SourceForge URL format incorrect. See https://github.com/Homebrew/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md # sourceforgeosdn-urls "
2016-09-24 13:52:43 +02:00
elsif bad_osdn_url?
2018-05-25 18:03:16 +02:00
add_warning " OSDN URL format incorrect. See https://github.com/Homebrew/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md # sourceforgeosdn-urls "
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def bad_url_format? ( regex , valid_formats_array )
return false unless cask . url . to_s =~ regex
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
valid_formats_array . none? { | format | cask . url . to_s =~ format }
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def bad_sourceforge_url?
2016-10-14 20:03:34 +02:00
bad_url_format? ( / sourceforge / ,
2016-09-24 13:52:43 +02:00
[
%r{ \ Ahttps://sourceforge \ .net/projects/[^/]+/files/latest/download \ Z } ,
%r{ \ Ahttps://downloads \ .sourceforge \ .net/(?!(project|sourceforge) \ /) } ,
] )
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def bad_osdn_url?
2016-10-14 20:03:34 +02:00
bad_url_format? ( / osd / , [ %r{ \ Ahttps?://([^/]+.)?dl \ .osdn \ .jp/ } ] )
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_generic_artifacts
2018-09-06 06:47:29 +02:00
cask . artifacts . select { | a | a . is_a? ( Artifact :: Artifact ) } . each do | artifact |
2017-04-06 00:33:31 +02:00
unless artifact . target . absolute?
add_error " target must be absolute path for #{ artifact . class . english_name } #{ artifact . source } "
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
end
end
2016-09-24 13:52:43 +02:00
def check_token_conflicts
return unless check_token_conflicts?
return unless core_formula_names . include? ( cask . token )
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
add_warning " possible duplicate, cask token conflicts with Homebrew core formula: #{ core_formula_url } "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def core_tap
@core_tap || = CoreTap . instance
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def core_formula_names
core_tap . formula_names
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def core_formula_url
" #{ core_tap . default_remote } /blob/master/Formula/ #{ cask . token } .rb "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_download
return unless download && cask . url
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
odebug " Auditing download "
downloaded_path = download . perform
Verify . all ( cask , downloaded_path )
rescue = > e
add_error " download not possible: #{ e . message } "
end
2018-10-10 21:36:02 +00:00
2019-05-07 17:06:54 +02:00
def check_appcast_contains_version
return unless check_appcast?
return if cask . appcast . to_s . empty?
2019-05-21 12:07:25 +02:00
return if cask . appcast . configuration == :no_check
2019-05-07 17:06:54 +02:00
appcast_stanza = cask . appcast . to_s
2019-05-21 14:51:06 +02:00
appcast_contents , = curl_output ( " --location " , " --max-time " , " 5 " , appcast_stanza )
2019-05-07 17:06:54 +02:00
version_stanza = cask . version . to_s
2019-05-21 14:51:06 +02:00
if cask . appcast . configuration . blank?
2019-05-21 12:07:25 +02:00
adjusted_version_stanza = version_stanza . split ( " , " ) [ 0 ] . split ( " - " ) [ 0 ] . split ( " _ " ) [ 0 ]
else
adjusted_version_stanza = cask . appcast . configuration
end
2019-05-07 17:06:54 +02:00
return if appcast_contents . include? adjusted_version_stanza
add_warning " appcast at URL ' #{ appcast_stanza } ' does not contain " \
" the version number: ' #{ adjusted_version_stanza } ' "
rescue
add_error " appcast at URL ' #{ appcast_stanza } ' offline or looping "
end
2018-10-10 21:36:02 +00:00
def check_https_availability
2018-10-10 21:36:06 +00:00
return unless download
2019-02-19 13:12:52 +00:00
2018-11-24 11:21:52 +00:00
if ! cask . url . blank? && ! cask . url . using
2018-10-10 21:36:06 +00:00
check_url_for_https_availability ( cask . url , user_agents : [ cask . url . user_agent ] )
end
2018-11-24 02:02:53 +00:00
check_url_for_https_availability ( cask . appcast ) unless cask . appcast . blank?
2018-12-13 15:16:10 +00:00
check_url_for_https_availability ( cask . homepage , user_agents : [ :browser ] ) unless cask . homepage . blank?
2018-10-10 21:36:02 +00:00
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 )
2018-11-24 11:21:52 +00:00
add_error problem if problem
2018-10-10 21:36:02 +00:00
end
2016-08-18 22:11:42 +03:00
end
end