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
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
2017-05-07 06:41:40 +02:00
def initialize ( cask , download : false , check_token_conflicts : false , commit_range : nil , command : SystemCommand )
2016-09-24 13:52:43 +02:00
@cask = cask
@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
2017-05-07 06:41:40 +02:00
check_version_and_checksum
2016-09-24 13:52:43 +02:00
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
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
if count > 1
2017-11-01 22:35:41 -03:00
add_warning " only a single uninstall_preflight 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 :: PostflightBlock ) &&
2018-09-02 16:15:09 +01:00
k . directives . key? ( :uninstall_postflight )
end
if count > 1
2017-11-01 22:35:41 -03:00
add_warning " only a single uninstall_postflight stanza is allowed "
end
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
2017-05-07 06:41:40 +02:00
def check_version_and_checksum
2018-09-10 19:35:08 +02:00
return if cask . sha256 == :no_check
2017-05-07 06:41:40 +02:00
return if @cask . sourcefile_path . nil?
2018-03-27 08:41:09 +10:00
tap = @cask . tap
2017-05-07 06:41:40 +02:00
return if tap . nil?
2017-05-10 20:46:39 +02:00
return if commit_range . nil?
2018-09-17 02:45:00 +02:00
2017-05-07 06:41:40 +02:00
previous_cask_contents = Git . last_revision_of_file ( tap . path , @cask . sourcefile_path , before_commit : commit_range )
return if previous_cask_contents . empty?
2017-10-01 02:13:53 +02:00
begin
2017-10-07 15:58:49 +02:00
previous_cask = CaskLoader . load ( previous_cask_contents )
2017-05-07 06:41:40 +02:00
2017-10-01 02:13:53 +02:00
return unless previous_cask . version == cask . version
return if previous_cask . sha256 == cask . sha256
2017-05-07 06:41:40 +02:00
2018-05-25 18:03:16 +02:00
add_error " only sha256 changed (see: https://github.com/Homebrew/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/sha256.md) "
2017-10-01 02:13:53 +02:00
rescue CaskError = > e
add_warning " Skipped version and checksum comparison. Reading previous version failed: #{ e } "
end
2017-05-07 06:41:40 +02: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
def check_https_availability
2018-10-10 21:36:06 +00:00
unless cask . url . to_s . empty? || cask . url . using
check_url_for_https_availability ( cask . url , user_agents : [ cask . url . user_agent ] )
end
2018-10-10 21:36:02 +00:00
check_url_for_https_availability ( cask . appcast ) unless cask . appcast . to_s . empty?
check_url_for_https_availability ( cask . homepage ) unless cask . homepage . to_s . empty?
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 unless problem . nil?
end
2016-08-18 22:11:42 +03:00
end
end