2023-02-24 16:20:51 -08:00
|
|
|
# typed: true
|
2019-04-19 15:38:03 +09:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2023-12-28 11:45:18 -08:00
|
|
|
require "attrable"
|
2020-06-06 21:10:16 +01:00
|
|
|
require "cask/denylist"
|
2018-09-03 19:39:07 +01:00
|
|
|
require "cask/download"
|
2016-08-18 22:11:42 +03:00
|
|
|
require "digest"
|
2020-12-14 14:30:36 +01:00
|
|
|
require "livecheck/livecheck"
|
2023-05-19 19:43:15 +02:00
|
|
|
require "source_location"
|
2023-09-18 12:38:10 -04:00
|
|
|
require "system_command"
|
2018-10-10 21:36:02 +00:00
|
|
|
require "utils/curl"
|
2017-05-07 06:41:40 +02:00
|
|
|
require "utils/git"
|
2020-08-26 09:42:39 +02:00
|
|
|
require "utils/shared_audits"
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2018-09-06 08:29:14 +02:00
|
|
|
module Cask
|
2020-08-24 21:32:40 +02:00
|
|
|
# Audit a cask for various problems.
|
|
|
|
#
|
|
|
|
# @api private
|
2016-09-24 13:52:43 +02:00
|
|
|
class Audit
|
2024-01-26 17:33:55 -08:00
|
|
|
include SystemCommand::Mixin
|
2023-09-06 00:12:57 +08:00
|
|
|
include ::Utils::Curl
|
2023-12-28 11:45:18 -08:00
|
|
|
extend Attrable
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2020-09-04 04:14:37 +02:00
|
|
|
attr_reader :cask, :download
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2023-03-29 20:49:29 +02:00
|
|
|
attr_predicate :new_cask?, :strict?, :signing?, :online?, :token_conflicts?
|
2019-05-07 17:06:54 +02:00
|
|
|
|
2023-02-05 15:22:06 +01:00
|
|
|
def initialize(
|
|
|
|
cask,
|
2023-03-29 20:49:29 +02:00
|
|
|
download: nil, quarantine: nil,
|
2023-02-05 15:22:06 +01:00
|
|
|
token_conflicts: nil, online: nil, strict: nil, signing: nil,
|
|
|
|
new_cask: nil, only: [], except: []
|
|
|
|
)
|
2022-08-16 10:01:35 +02:00
|
|
|
# `new_cask` implies `online`, `token_conflicts`, `strict` and `signing`
|
2020-09-04 04:14:37 +02:00
|
|
|
online = new_cask if online.nil?
|
|
|
|
strict = new_cask if strict.nil?
|
2022-08-01 14:30:04 +02:00
|
|
|
signing = new_cask if signing.nil?
|
|
|
|
token_conflicts = new_cask if token_conflicts.nil?
|
2020-09-04 04:14:37 +02:00
|
|
|
|
2023-03-29 20:49:29 +02:00
|
|
|
# `online` and `signing` imply `download`
|
|
|
|
download = online || signing if download.nil?
|
2020-09-04 04:14:37 +02:00
|
|
|
|
2016-09-24 13:52:43 +02:00
|
|
|
@cask = cask
|
2024-03-07 16:20:20 +00:00
|
|
|
@download = Download.new(cask, quarantine:) if download
|
2020-04-23 21:16:17 +02:00
|
|
|
@online = online
|
|
|
|
@strict = strict
|
2022-08-01 14:30:04 +02:00
|
|
|
@signing = signing
|
2020-04-23 21:16:17 +02:00
|
|
|
@new_cask = new_cask
|
|
|
|
@token_conflicts = token_conflicts
|
2023-02-05 15:22:06 +01:00
|
|
|
@only = only || []
|
|
|
|
@except = except || []
|
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 run!
|
2022-09-13 10:54:05 +02:00
|
|
|
only_audits = @only
|
|
|
|
except_audits = @except
|
|
|
|
|
2023-02-05 15:22:06 +01:00
|
|
|
private_methods.map(&:to_s).grep(/^audit_/).each do |audit_method_name|
|
|
|
|
name = audit_method_name.delete_prefix("audit_")
|
2022-09-13 10:54:05 +02:00
|
|
|
next if !only_audits.empty? && only_audits&.exclude?(name)
|
|
|
|
next if except_audits&.include?(name)
|
|
|
|
|
|
|
|
send(audit_method_name)
|
|
|
|
end
|
|
|
|
|
2016-09-24 13:52:43 +02:00
|
|
|
self
|
2018-09-02 20:14:54 +01:00
|
|
|
rescue => e
|
2023-09-11 21:54:27 -07:00
|
|
|
odebug e, ::Utils::Backtrace.clean(e)
|
2016-09-24 13:52:43 +02:00
|
|
|
add_error "exception while auditing #{cask}: #{e.message}"
|
|
|
|
self
|
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2020-07-28 09:08:37 +02:00
|
|
|
def errors
|
|
|
|
@errors ||= []
|
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def errors?
|
|
|
|
errors.any?
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def success?
|
2023-03-30 23:52:24 +01:00
|
|
|
!errors?
|
2022-09-13 10:54:05 +02:00
|
|
|
end
|
|
|
|
|
2023-05-19 19:43:15 +02:00
|
|
|
sig {
|
|
|
|
params(
|
|
|
|
message: T.nilable(String),
|
|
|
|
location: T.nilable(Homebrew::SourceLocation),
|
|
|
|
strict_only: T::Boolean,
|
|
|
|
).void
|
|
|
|
}
|
2023-04-04 17:22:00 +01:00
|
|
|
def add_error(message, location: nil, strict_only: false)
|
2023-03-31 01:25:36 +01:00
|
|
|
# Only raise non-critical audits if the user specified `--strict`.
|
2023-04-04 17:22:00 +01:00
|
|
|
return if strict_only && !@strict
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2024-03-07 16:20:20 +00:00
|
|
|
errors << ({ message:, location:, corrected: false })
|
2020-07-28 09:08:37 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
def result
|
2023-03-30 23:52:24 +01:00
|
|
|
Formatter.error("failed") if errors?
|
2020-07-28 09:08:37 +02:00
|
|
|
end
|
|
|
|
|
2023-03-30 23:52:24 +01:00
|
|
|
sig { returns(T.nilable(String)) }
|
|
|
|
def summary
|
2023-03-29 00:40:46 +01:00
|
|
|
return if success?
|
2021-03-21 13:59:43 -04:00
|
|
|
|
2020-07-28 09:08:37 +02:00
|
|
|
summary = ["audit for #{cask}: #{result}"]
|
|
|
|
|
|
|
|
errors.each do |error|
|
2021-04-03 03:49:41 +02:00
|
|
|
summary << " #{Formatter.error("-")} #{error[:message]}"
|
2020-07-28 09:08:37 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
summary.join("\n")
|
|
|
|
end
|
|
|
|
|
2016-09-24 13:52:43 +02:00
|
|
|
private
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_untrusted_pkg
|
2018-03-25 15:30:16 +10:00
|
|
|
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
|
|
|
|
2021-01-07 13:49:05 -08:00
|
|
|
return if cask.artifacts.none? { |k| k.is_a?(Artifact::Pkg) && k.stanza_options.key?(:allow_untrusted) }
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "allow_untrusted is not permitted in official Homebrew Cask taps"
|
2018-03-25 15:30:16 +10:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_stanza_requires_uninstall
|
2018-05-19 12:38:47 +10:00
|
|
|
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) }
|
2021-03-01 13:43:47 +00:00
|
|
|
return if cask.artifacts.any?(Artifact::Uninstall)
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "installer and pkg stanzas require an uninstall stanza"
|
2018-05-19 12:38:47 +10:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_single_pre_postflight
|
2017-10-30 20:47:22 -03:00
|
|
|
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
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "only a single preflight stanza is allowed"
|
2017-11-01 22:35:41 -03:00
|
|
|
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
|
2023-04-18 15:06:50 -07:00
|
|
|
return if count <= 1
|
2018-09-02 16:15:09 +01:00
|
|
|
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "only a single postflight stanza is allowed"
|
2017-10-30 20:47:22 -03:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_single_uninstall_zap
|
2017-10-27 16:53:22 -03:00
|
|
|
odebug "Auditing single uninstall_* and zap stanzas"
|
|
|
|
|
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
|
|
|
|
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "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
|
|
|
|
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "only a single uninstall_postflight stanza is allowed" if count > 1
|
2017-10-27 16:53:22 -03:00
|
|
|
|
2023-04-18 15:06:50 -07:00
|
|
|
return if cask.artifacts.count { |k| k.is_a?(Artifact::Zap) } <= 1
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "only a single zap stanza is allowed"
|
2017-10-27 16:53:22 -03:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_required_stanzas
|
2016-09-24 13:52:43 +02:00
|
|
|
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
|
2020-09-11 10:29:21 +01:00
|
|
|
rejected_artifacts = [:uninstall, :zap]
|
|
|
|
installable_artifacts = cask.artifacts.reject { |k| rejected_artifacts.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
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_description
|
|
|
|
# Fonts seldom benefit from descriptions and requiring them disproportionately
|
|
|
|
# increases the maintenance burden.
|
2022-09-13 10:54:05 +02:00
|
|
|
return if cask.tap == "homebrew/cask-fonts"
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2023-04-04 17:22:00 +01:00
|
|
|
add_error("Cask should have a description. Please add a `desc` stanza.", strict_only: true) if cask.desc.blank?
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2023-02-13 21:15:59 +01:00
|
|
|
sig { void }
|
|
|
|
def audit_version_special_characters
|
|
|
|
return unless cask.version
|
|
|
|
|
|
|
|
return if cask.version.latest?
|
|
|
|
|
|
|
|
raw_version = cask.version.raw_version
|
|
|
|
return if raw_version.exclude?(":") && raw_version.exclude?("/")
|
|
|
|
|
|
|
|
add_error "version should not contain colons or slashes"
|
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_no_string_version_latest
|
2022-09-13 10:54:05 +02:00
|
|
|
return unless cask.version
|
|
|
|
|
|
|
|
odebug "Auditing version :latest does not appear as a string ('latest')"
|
2023-04-18 15:06:50 -07:00
|
|
|
return if 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
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_sha256_no_check_if_latest
|
2016-09-24 13:52:43 +02:00
|
|
|
return unless cask.sha256
|
2023-09-06 22:30:43 -07:00
|
|
|
return unless cask.version
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
odebug "Auditing 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
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_sha256_no_check_if_unversioned
|
2022-09-13 10:54:05 +02:00
|
|
|
return unless cask.sha256
|
2020-12-07 23:02:55 +01:00
|
|
|
return if cask.sha256 == :no_check
|
|
|
|
|
2023-05-19 19:43:15 +02:00
|
|
|
return unless cask.url&.unversioned?
|
|
|
|
|
|
|
|
add_error "Use `sha256 :no_check` when URL is unversioned."
|
2020-12-07 23:02:55 +01:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_sha256_actually_256
|
2022-09-13 10:54:05 +02:00
|
|
|
return unless cask.sha256
|
|
|
|
|
|
|
|
odebug "Auditing sha256 string is a legal SHA-256 digest"
|
2020-11-19 18:12:16 +01:00
|
|
|
return unless cask.sha256.is_a?(Checksum)
|
|
|
|
return if cask.sha256.length == 64 && cask.sha256[/^[0-9a-f]+$/i]
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2020-11-19 18:12:16 +01:00
|
|
|
add_error "sha256 string must be of 64 hexadecimal characters"
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_sha256_invalid
|
2022-09-13 10:54:05 +02:00
|
|
|
return unless cask.sha256
|
|
|
|
|
|
|
|
odebug "Auditing sha256 is not a known invalid value"
|
2016-09-24 13:52:43 +02:00
|
|
|
empty_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
2023-04-18 15:06:50 -07:00
|
|
|
return if cask.sha256 != empty_sha256
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2020-11-19 18:12:16 +01:00
|
|
|
add_error "cannot use the sha256 for an empty string: #{empty_sha256}"
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-03-29 20:49:29 +02:00
|
|
|
def audit_latest_with_livecheck
|
2023-09-06 22:30:43 -07:00
|
|
|
return unless cask.version&.latest?
|
2023-02-28 10:29:28 +01:00
|
|
|
return unless cask.livecheckable?
|
|
|
|
return if cask.livecheck.skip?
|
|
|
|
|
|
|
|
add_error "Casks with a `livecheck` should not use `version :latest`."
|
2018-03-27 20:56:01 +10:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_latest_with_auto_updates
|
2023-09-06 22:30:43 -07:00
|
|
|
return unless cask.version&.latest?
|
2018-07-12 16:13:46 +10:00
|
|
|
return unless cask.auto_updates
|
|
|
|
|
2021-01-08 03:48:53 +01:00
|
|
|
add_error "Casks with `version :latest` should not use `auto_updates`."
|
2018-07-12 16:13:46 +10:00
|
|
|
end
|
|
|
|
|
2021-06-23 13:23:18 -07:00
|
|
|
LIVECHECK_REFERENCE_URL = "https://docs.brew.sh/Cask-Cookbook#stanza-livecheck"
|
2021-01-28 16:17:40 -08:00
|
|
|
|
2023-02-24 16:20:51 -08:00
|
|
|
sig { params(livecheck_result: T.any(NilClass, T::Boolean, Symbol)).void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_hosting_with_livecheck(livecheck_result: audit_livecheck_version)
|
2023-12-17 18:07:51 -05:00
|
|
|
return if cask.discontinued? || cask.deprecated? || cask.disabled?
|
2023-09-06 22:30:43 -07:00
|
|
|
return if cask.version&.latest?
|
|
|
|
return unless cask.url
|
2023-03-29 20:49:29 +02:00
|
|
|
return if block_url_offline?
|
|
|
|
return if cask.livecheckable?
|
2021-03-31 07:17:37 +02:00
|
|
|
return if livecheck_result == :auto_detected
|
2018-06-05 16:42:15 +10:00
|
|
|
|
2021-03-31 07:17:37 +02:00
|
|
|
add_livecheck = "please add a livecheck. See #{Formatter.url(LIVECHECK_REFERENCE_URL)}"
|
2018-06-15 17:01:27 +10:00
|
|
|
|
|
|
|
case cask.url.to_s
|
|
|
|
when %r{sourceforge.net/(\S+)}
|
2021-04-07 02:19:16 +02:00
|
|
|
return unless online?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error "Download is hosted on SourceForge, #{add_livecheck}", location: cask.url.location
|
2018-06-15 17:01:27 +10:00
|
|
|
when %r{dl.devmate.com/(\S+)}
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error "Download is hosted on DevMate, #{add_livecheck}", location: cask.url.location
|
2018-06-15 17:01:27 +10:00
|
|
|
when %r{rink.hockeyapp.net/(\S+)}
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error "Download is hosted on HockeyApp, #{add_livecheck}", location: cask.url.location
|
2018-06-15 17:01:27 +10:00
|
|
|
end
|
2018-06-05 16:42:15 +10:00
|
|
|
end
|
|
|
|
|
2022-09-06 11:46:40 +01:00
|
|
|
SOURCEFORGE_OSDN_REFERENCE_URL = "https://docs.brew.sh/Cask-Cookbook#sourceforgeosdn-urls"
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_download_url_format
|
2022-09-13 10:54:05 +02:00
|
|
|
return unless cask.url
|
2023-08-29 17:10:40 -04:00
|
|
|
return if block_url_offline?
|
2022-09-13 10:54:05 +02:00
|
|
|
|
2016-09-24 13:52:43 +02:00
|
|
|
odebug "Auditing URL format"
|
|
|
|
if bad_sourceforge_url?
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error "SourceForge URL format incorrect. See #{Formatter.url(SOURCEFORGE_OSDN_REFERENCE_URL)}",
|
|
|
|
location: cask.url.location
|
2016-09-24 13:52:43 +02:00
|
|
|
elsif bad_osdn_url?
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error "OSDN URL format incorrect. See #{Formatter.url(SOURCEFORGE_OSDN_REFERENCE_URL)}",
|
|
|
|
location: cask.url.location
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
end
|
|
|
|
|
2021-11-07 01:23:31 +00:00
|
|
|
VERIFIED_URL_REFERENCE_URL = "https://docs.brew.sh/Cask-Cookbook#when-url-and-homepage-domains-differ-add-verified"
|
2021-01-28 16:17:40 -08:00
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_unnecessary_verified
|
2023-09-06 22:30:43 -07:00
|
|
|
return unless cask.url
|
2021-03-31 06:15:06 +02:00
|
|
|
return if block_url_offline?
|
2020-09-08 22:12:26 +08:00
|
|
|
return unless verified_present?
|
|
|
|
return unless url_match_homepage?
|
|
|
|
return unless verified_matches_url?
|
|
|
|
|
2021-01-28 16:17:40 -08:00
|
|
|
add_error "The URL's domain #{Formatter.url(domain)} matches the homepage domain " \
|
|
|
|
"#{Formatter.url(homepage)}, the 'verified' parameter of the 'url' stanza is unnecessary. " \
|
|
|
|
"See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}"
|
2020-09-08 22:12:26 +08:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_missing_verified
|
2023-09-06 22:30:43 -07:00
|
|
|
return unless cask.url
|
2021-03-31 06:15:06 +02:00
|
|
|
return if block_url_offline?
|
2020-12-12 05:55:39 +01:00
|
|
|
return if file_url?
|
2020-09-08 22:12:26 +08:00
|
|
|
return if url_match_homepage?
|
|
|
|
return if verified_present?
|
|
|
|
|
2021-01-28 16:17:40 -08:00
|
|
|
add_error "The URL's domain #{Formatter.url(domain)} does not match the homepage domain " \
|
|
|
|
"#{Formatter.url(homepage)}, a 'verified' parameter has to be added to the 'url' stanza. " \
|
|
|
|
"See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}"
|
2020-09-08 22:12:26 +08:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_no_match
|
2023-09-06 22:30:43 -07:00
|
|
|
return unless cask.url
|
2021-03-31 06:15:06 +02:00
|
|
|
return if block_url_offline?
|
2020-09-08 22:12:26 +08:00
|
|
|
return unless verified_present?
|
2021-01-28 15:27:00 -08:00
|
|
|
return if verified_matches_url?
|
2020-09-08 22:12:26 +08:00
|
|
|
|
2021-01-28 16:17:40 -08:00
|
|
|
add_error "Verified URL #{Formatter.url(url_from_verified)} does not match URL " \
|
|
|
|
"#{Formatter.url(strip_url_scheme(cask.url.to_s))}. " \
|
2023-05-19 19:43:15 +02:00
|
|
|
"See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}",
|
|
|
|
location: cask.url.location
|
2020-09-08 22:12:26 +08:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_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
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_languages
|
2020-06-04 23:37:54 +02:00
|
|
|
@cask.languages.each do |language|
|
2020-08-12 00:04:20 +02:00
|
|
|
Locale.parse(language)
|
|
|
|
rescue Locale::ParserError
|
|
|
|
add_error "Locale '#{language}' is invalid."
|
2020-06-04 23:37:54 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_token_conflicts
|
2020-09-04 05:29:56 +02:00
|
|
|
return unless token_conflicts?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2023-08-04 16:21:31 +01:00
|
|
|
Homebrew.with_no_api_env do
|
|
|
|
return unless core_formula_names.include?(cask.token)
|
|
|
|
|
|
|
|
add_error(
|
|
|
|
"possible duplicate, cask token conflicts with Homebrew core formula: #{Formatter.url(core_formula_url)}",
|
|
|
|
strict_only: true,
|
|
|
|
)
|
|
|
|
end
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_token_valid
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "cask token contains non-ascii characters" unless cask.token.ascii_only?
|
|
|
|
add_error "cask token + should be replaced by -plus-" if cask.token.include? "+"
|
|
|
|
add_error "cask token whitespace should be replaced by hyphens" if cask.token.include? " "
|
|
|
|
add_error "cask token @ should be replaced by -at-" if cask.token.include? "@"
|
|
|
|
add_error "cask token underscores should be replaced by hyphens" if cask.token.include? "_"
|
|
|
|
add_error "cask token should not contain double hyphens" if cask.token.include? "--"
|
2020-06-04 23:11:51 +02:00
|
|
|
|
2022-12-13 10:54:22 +00:00
|
|
|
if cask.token.match?(/[^a-z0-9-]/)
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "cask token should only contain lowercase alphanumeric characters and hyphens"
|
2020-06-04 23:11:51 +02:00
|
|
|
end
|
|
|
|
|
2021-01-07 13:49:05 -08:00
|
|
|
return if !cask.token.start_with?("-") && !cask.token.end_with?("-")
|
2020-06-04 23:11:51 +02:00
|
|
|
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "cask token should not have leading or trailing hyphens"
|
2020-06-04 23:11:51 +02:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_token_bad_words
|
2020-09-14 02:55:47 +02:00
|
|
|
return unless new_cask?
|
2020-06-04 23:11:51 +02:00
|
|
|
|
|
|
|
token = cask.token
|
|
|
|
|
2020-09-14 01:26:06 +02:00
|
|
|
add_error "cask token contains .app" if token.end_with? ".app"
|
2020-06-04 23:11:51 +02:00
|
|
|
|
2023-02-24 16:20:51 -08:00
|
|
|
match_data = /-(?<designation>alpha|beta|rc|release-candidate)$/.match(cask.token)
|
|
|
|
if match_data && cask.tap&.official? && cask.tap != "homebrew/cask-versions"
|
|
|
|
add_error "cask token contains version designation '#{match_data[:designation]}'"
|
2020-06-04 23:11:51 +02:00
|
|
|
end
|
|
|
|
|
2023-04-04 17:22:00 +01:00
|
|
|
add_error("cask token mentions launcher", strict_only: true) if token.end_with? "launcher"
|
2020-06-04 23:11:51 +02:00
|
|
|
|
2023-04-04 17:22:00 +01:00
|
|
|
add_error("cask token mentions desktop", strict_only: true) if token.end_with? "desktop"
|
2020-06-04 23:11:51 +02:00
|
|
|
|
2023-04-04 17:22:00 +01:00
|
|
|
add_error("cask token mentions platform", strict_only: true) if token.end_with? "mac", "osx", "macos"
|
2020-06-04 23:11:51 +02:00
|
|
|
|
2023-04-04 17:22:00 +01:00
|
|
|
add_error("cask token mentions architecture", strict_only: true) if token.end_with? "x86", "32_bit", "x86_64",
|
|
|
|
"64_bit"
|
2020-06-04 23:11:51 +02:00
|
|
|
|
2021-01-07 13:49:05 -08:00
|
|
|
frameworks = %w[cocoa qt gtk wx java]
|
|
|
|
return if frameworks.include?(token) || !token.end_with?(*frameworks)
|
2020-06-04 23:11:51 +02:00
|
|
|
|
2023-04-04 17:22:00 +01:00
|
|
|
add_error("cask token mentions framework", strict_only: true)
|
2020-06-04 23:11:51 +02:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_download
|
2021-01-07 13:49:05 -08:00
|
|
|
return if download.blank? || cask.url.blank?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2023-05-19 19:43:15 +02:00
|
|
|
begin
|
|
|
|
download.fetch
|
|
|
|
rescue => e
|
|
|
|
add_error "download not possible: #{e}", location: cask.url.location
|
|
|
|
end
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
2018-10-10 21:36:02 +00:00
|
|
|
|
2023-02-21 17:02:01 +01:00
|
|
|
sig { void }
|
2023-03-29 20:49:29 +02:00
|
|
|
def audit_livecheck_unneeded_long_version
|
2023-09-06 22:30:43 -07:00
|
|
|
return if cask.version.nil? || cask.url.nil?
|
2023-04-18 15:06:50 -07:00
|
|
|
return if cask.livecheck.strategy != :sparkle
|
2023-02-21 17:02:01 +01:00
|
|
|
return unless cask.version.csv.second
|
|
|
|
return if cask.url.to_s.include? cask.version.csv.second
|
2023-02-22 19:43:02 +01:00
|
|
|
return if cask.version.csv.third.present? && cask.url.to_s.include?(cask.version.csv.third)
|
2023-02-21 17:02:01 +01:00
|
|
|
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error "Download does not require additional version components. Use `&:short_version` in the livecheck",
|
|
|
|
location: cask.url.location,
|
|
|
|
strict_only: true
|
2023-02-21 17:02:01 +01:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_signing
|
2022-08-01 14:30:04 +02:00
|
|
|
return if !signing? || download.blank? || cask.url.blank?
|
|
|
|
|
|
|
|
odebug "Auditing signing"
|
2023-03-04 15:55:39 +01:00
|
|
|
|
2023-11-05 17:18:21 -05:00
|
|
|
extract_artifacts do |artifacts, tmpdir|
|
2022-08-17 09:56:45 +02:00
|
|
|
artifacts.each do |artifact|
|
2023-04-13 18:48:07 +01:00
|
|
|
artifact_path = artifact.is_a?(Artifact::Pkg) ? artifact.path : artifact.source
|
|
|
|
path = tmpdir/artifact_path.relative_path_from(cask.staged_path)
|
|
|
|
|
|
|
|
next unless path.exist?
|
|
|
|
|
|
|
|
result = system_command("spctl", args: ["--assess", "--type", "install", path], print_stderr: false)
|
|
|
|
|
|
|
|
next if result.success?
|
|
|
|
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error <<~EOS, location: cask.url.location, strict_only: true
|
2023-04-13 18:48:07 +01:00
|
|
|
Signature verification failed:
|
|
|
|
#{result.merged_output}
|
|
|
|
macOS on ARM requires software to be signed.
|
|
|
|
Please contact the upstream developer to let them know they should sign and notarize their software.
|
|
|
|
EOS
|
2022-08-01 14:30:04 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-11-05 17:18:21 -05:00
|
|
|
sig { void }
|
|
|
|
def extract_artifacts
|
|
|
|
return unless online?
|
|
|
|
|
|
|
|
artifacts = cask.artifacts.select do |artifact|
|
|
|
|
artifact.is_a?(Artifact::Pkg) || artifact.is_a?(Artifact::App) || artifact.is_a?(Artifact::Binary)
|
|
|
|
end
|
|
|
|
|
|
|
|
if @artifacts_extracted && @tmpdir
|
|
|
|
yield artifacts, @tmpdir if block_given?
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
return if artifacts.empty?
|
|
|
|
|
2024-02-26 16:58:39 +00:00
|
|
|
@tmpdir ||= Pathname(Dir.mktmpdir("cask-audit", HOMEBREW_TEMP))
|
2023-11-05 17:18:21 -05:00
|
|
|
|
|
|
|
ohai "Downloading and extracting artifacts"
|
|
|
|
|
|
|
|
downloaded_path = download.fetch
|
|
|
|
|
|
|
|
primary_container = UnpackStrategy.detect(downloaded_path, type: @cask.container&.type, merge_xattrs: true)
|
|
|
|
return if primary_container.nil?
|
|
|
|
|
|
|
|
# Extract the container to the temporary directory.
|
|
|
|
primary_container.extract_nestedly(to: @tmpdir, basename: downloaded_path.basename, verbose: false)
|
|
|
|
@artifacts_extracted = true # Set the flag to indicate that extraction has occurred.
|
|
|
|
|
|
|
|
# Yield the artifacts and temp directory to the block if provided.
|
|
|
|
yield artifacts, @tmpdir if block_given?
|
|
|
|
end
|
|
|
|
|
2023-02-24 16:20:51 -08:00
|
|
|
sig { returns(T.any(NilClass, T::Boolean, Symbol)) }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_livecheck_version
|
2023-03-29 20:49:29 +02:00
|
|
|
return unless online?
|
2023-09-06 22:30:43 -07:00
|
|
|
return unless cask.version
|
2021-06-14 12:05:32 -04:00
|
|
|
|
2021-11-02 12:09:56 -04:00
|
|
|
referenced_cask, = Homebrew::Livecheck.resolve_livecheck_reference(cask)
|
|
|
|
|
|
|
|
# Respect skip conditions for a referenced cask
|
|
|
|
if referenced_cask
|
|
|
|
skip_info = Homebrew::Livecheck::SkipConditions.referenced_skip_information(
|
|
|
|
referenced_cask,
|
|
|
|
Homebrew::Livecheck.cask_name(cask),
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2023-12-04 13:43:33 -05:00
|
|
|
# Respect cask skip conditions (e.g. deprecated, disabled, latest, unversioned)
|
2021-11-02 12:09:56 -04:00
|
|
|
skip_info ||= Homebrew::Livecheck::SkipConditions.skip_information(cask)
|
2021-06-14 12:05:32 -04:00
|
|
|
return :skip if skip_info.present?
|
2020-12-14 14:30:36 +01:00
|
|
|
|
2021-11-02 12:09:56 -04:00
|
|
|
latest_version = Homebrew::Livecheck.latest_version(
|
|
|
|
cask,
|
|
|
|
referenced_formula_or_cask: referenced_cask,
|
|
|
|
)&.fetch(:latest)
|
2021-03-20 23:38:20 +01:00
|
|
|
|
2023-03-29 20:49:29 +02:00
|
|
|
return :auto_detected if cask.version.to_s == latest_version.to_s
|
2020-12-14 14:30:36 +01:00
|
|
|
|
|
|
|
add_error "Version '#{cask.version}' differs from '#{latest_version}' retrieved by livecheck."
|
2021-03-31 07:17:37 +02:00
|
|
|
|
|
|
|
false
|
2020-12-14 14:30:36 +01:00
|
|
|
end
|
|
|
|
|
2023-09-18 12:38:10 -04:00
|
|
|
sig { void }
|
|
|
|
def audit_min_os
|
|
|
|
return unless online?
|
2023-10-26 15:45:06 -04:00
|
|
|
return unless strict?
|
2023-09-18 12:38:10 -04:00
|
|
|
|
|
|
|
odebug "Auditing minimum OS version"
|
|
|
|
|
|
|
|
plist_min_os = cask_plist_min_os
|
2023-11-05 17:18:21 -05:00
|
|
|
sparkle_min_os = livecheck_min_os
|
|
|
|
|
|
|
|
debug_messages = []
|
|
|
|
debug_messages << "Plist #{plist_min_os}" if plist_min_os
|
|
|
|
debug_messages << "Sparkle #{sparkle_min_os}" if sparkle_min_os
|
|
|
|
odebug "Minimum OS version: #{debug_messages.join(" | ")}" unless debug_messages.empty?
|
2023-11-08 15:21:15 -05:00
|
|
|
min_os = [plist_min_os, sparkle_min_os].compact.max
|
2023-09-18 12:38:10 -04:00
|
|
|
|
2023-11-05 17:18:21 -05:00
|
|
|
return if min_os.nil? || min_os <= HOMEBREW_MACOS_OLDEST_ALLOWED
|
2023-09-18 12:38:10 -04:00
|
|
|
|
|
|
|
cask_min_os = cask.depends_on.macos&.version
|
2023-11-05 17:18:21 -05:00
|
|
|
return if cask_min_os == min_os
|
2023-09-18 12:38:10 -04:00
|
|
|
|
|
|
|
min_os_symbol = if cask_min_os.present?
|
|
|
|
cask_min_os.to_sym.inspect
|
|
|
|
else
|
|
|
|
"no minimum OS version"
|
|
|
|
end
|
2023-11-05 17:18:21 -05:00
|
|
|
add_error "Upstream defined #{min_os.to_sym.inspect} as the minimum OS version " \
|
2023-09-18 12:38:10 -04:00
|
|
|
"and the cask defined #{min_os_symbol}",
|
|
|
|
strict_only: true
|
|
|
|
end
|
|
|
|
|
2023-11-05 17:18:21 -05:00
|
|
|
sig { returns(T.nilable(MacOSVersion)) }
|
2023-09-18 12:38:10 -04:00
|
|
|
def livecheck_min_os
|
2022-10-31 09:00:43 +01:00
|
|
|
return unless online?
|
2022-10-30 15:00:56 +01:00
|
|
|
return unless cask.livecheckable?
|
2023-04-18 15:06:50 -07:00
|
|
|
return if cask.livecheck.strategy != :sparkle
|
2022-10-30 15:00:56 +01:00
|
|
|
|
2023-11-08 09:13:09 -05:00
|
|
|
# `Sparkle` strategy blocks that use the `items` argument (instead of
|
|
|
|
# `item`) contain arbitrary logic that ignores/overrides the strategy's
|
|
|
|
# sorting, so we can't identify which item would be first/newest here.
|
|
|
|
return if cask.livecheck.strategy_block.present? &&
|
|
|
|
cask.livecheck.strategy_block.parameters[0] == [:opt, :items]
|
|
|
|
|
2023-11-08 09:29:10 -05:00
|
|
|
content = Homebrew::Livecheck::Strategy.page_content(cask.livecheck.url)[:content]
|
|
|
|
return if content.blank?
|
2022-10-30 15:00:56 +01:00
|
|
|
|
2022-11-01 20:47:51 +01:00
|
|
|
begin
|
2023-11-08 09:29:10 -05:00
|
|
|
items = Homebrew::Livecheck::Strategy::Sparkle.sort_items(
|
|
|
|
Homebrew::Livecheck::Strategy::Sparkle.filter_items(
|
|
|
|
Homebrew::Livecheck::Strategy::Sparkle.items_from_content(content),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
rescue
|
|
|
|
return
|
2022-11-01 20:47:51 +01:00
|
|
|
end
|
2023-11-08 09:29:10 -05:00
|
|
|
return if items.blank?
|
|
|
|
|
|
|
|
min_os = items[0]&.minimum_system_version&.strip_patch
|
|
|
|
# Big Sur is sometimes identified as 10.16, so we override it to the
|
|
|
|
# expected macOS version (11).
|
|
|
|
min_os = MacOSVersion.new("11") if min_os == "10.16"
|
|
|
|
min_os
|
2023-09-18 12:38:10 -04:00
|
|
|
end
|
2022-11-01 20:47:51 +01:00
|
|
|
|
2023-11-05 17:18:21 -05:00
|
|
|
sig { returns(T.nilable(MacOSVersion)) }
|
2023-09-18 12:38:10 -04:00
|
|
|
def cask_plist_min_os
|
|
|
|
return unless online?
|
2022-11-01 21:13:03 +01:00
|
|
|
|
2023-09-18 12:38:10 -04:00
|
|
|
plist_min_os = T.let(nil, T.untyped)
|
2023-11-05 17:18:21 -05:00
|
|
|
@staged_path ||= cask.staged_path
|
2023-09-18 12:38:10 -04:00
|
|
|
|
2023-11-05 17:18:21 -05:00
|
|
|
extract_artifacts do |artifacts, tmpdir|
|
2023-09-18 12:38:10 -04:00
|
|
|
artifacts.each do |artifact|
|
|
|
|
artifact_path = artifact.is_a?(Artifact::Pkg) ? artifact.path : artifact.source
|
|
|
|
path = tmpdir/artifact_path.relative_path_from(cask.staged_path)
|
|
|
|
plist_path = "#{path}/Contents/Info.plist"
|
|
|
|
next unless File.exist?(plist_path)
|
|
|
|
|
|
|
|
plist = system_command!("plutil", args: ["-convert", "xml1", "-o", "-", plist_path]).plist
|
|
|
|
plist_min_os = plist["LSMinimumSystemVersion"].presence
|
|
|
|
break if plist_min_os
|
|
|
|
end
|
|
|
|
end
|
2023-10-26 15:45:06 -04:00
|
|
|
|
2023-09-18 12:38:10 -04:00
|
|
|
begin
|
|
|
|
MacOSVersion.new(plist_min_os).strip_patch
|
|
|
|
rescue MacOSVersion::Error
|
|
|
|
nil
|
2022-12-28 21:31:02 -08:00
|
|
|
end
|
2022-10-30 15:00:56 +01:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_github_prerelease_version
|
2020-09-03 17:35:00 +02:00
|
|
|
return if cask.tap == "homebrew/cask-versions"
|
|
|
|
|
2020-08-13 16:17:47 +02:00
|
|
|
odebug "Auditing GitHub prerelease"
|
2021-03-31 06:15:06 +02:00
|
|
|
user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*}) if online?
|
2020-08-13 16:17:47 +02:00
|
|
|
return if user.nil?
|
|
|
|
|
2020-09-09 08:57:56 -07:00
|
|
|
tag = SharedAudits.github_tag_from_url(cask.url)
|
|
|
|
tag ||= cask.version
|
2024-03-07 16:20:20 +00:00
|
|
|
error = SharedAudits.github_release(user, repo, tag, cask:)
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error error, location: cask.url.location if error
|
2020-08-13 16:17:47 +02:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_gitlab_prerelease_version
|
2020-09-03 17:35:00 +02:00
|
|
|
return if cask.tap == "homebrew/cask-versions"
|
|
|
|
|
2021-03-31 06:15:06 +02:00
|
|
|
user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*}) if online?
|
2020-08-13 16:17:47 +02:00
|
|
|
return if user.nil?
|
|
|
|
|
|
|
|
odebug "Auditing GitLab prerelease"
|
|
|
|
|
2020-09-09 08:57:56 -07:00
|
|
|
tag = SharedAudits.gitlab_tag_from_url(cask.url)
|
|
|
|
tag ||= cask.version
|
2024-03-07 16:20:20 +00:00
|
|
|
error = SharedAudits.gitlab_release(user, repo, tag, cask:)
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error error, location: cask.url.location if error
|
2020-08-13 16:17:47 +02:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_github_repository_archived
|
2023-12-04 13:43:33 -05:00
|
|
|
# Deprecated/disabled casks may have an archived repo.
|
2023-12-17 18:07:51 -05:00
|
|
|
return if cask.discontinued? || cask.deprecated? || cask.disabled?
|
2023-04-14 18:55:29 +02:00
|
|
|
|
2021-03-31 06:15:06 +02:00
|
|
|
user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*}) if online?
|
2020-08-13 16:17:47 +02:00
|
|
|
return if user.nil?
|
|
|
|
|
|
|
|
metadata = SharedAudits.github_repo_data(user, repo)
|
|
|
|
return if metadata.nil?
|
2020-11-07 21:29:06 +01:00
|
|
|
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error "GitHub repo is archived", location: cask.url.location if metadata["archived"]
|
2020-08-13 16:17:47 +02:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_gitlab_repository_archived
|
2023-12-04 13:43:33 -05:00
|
|
|
# Deprecated/disabled casks may have an archived repo.
|
2023-12-17 18:07:51 -05:00
|
|
|
return if cask.discontinued? || cask.deprecated? || cask.disabled?
|
2023-04-14 18:55:29 +02:00
|
|
|
|
2021-03-31 06:15:06 +02:00
|
|
|
user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*}) if online?
|
2020-08-13 16:17:47 +02:00
|
|
|
return if user.nil?
|
|
|
|
|
|
|
|
odebug "Auditing GitLab repo archived"
|
|
|
|
|
|
|
|
metadata = SharedAudits.gitlab_repo_data(user, repo)
|
|
|
|
return if metadata.nil?
|
2020-11-07 21:29:06 +01:00
|
|
|
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error "GitLab repo is archived", location: cask.url.location if metadata["archived"]
|
2020-08-13 16:17:47 +02:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_github_repository
|
2020-09-21 04:40:32 -05:00
|
|
|
return unless new_cask?
|
2020-08-13 16:17:47 +02:00
|
|
|
|
2020-04-23 21:16:17 +02:00
|
|
|
user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*})
|
|
|
|
return if user.nil?
|
|
|
|
|
|
|
|
odebug "Auditing GitHub repo"
|
|
|
|
|
|
|
|
error = SharedAudits.github(user, repo)
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error error, location: cask.url.location if error
|
2020-04-23 21:16:17 +02:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_gitlab_repository
|
2020-09-02 19:13:46 +02:00
|
|
|
return unless new_cask?
|
2020-08-13 16:17:47 +02:00
|
|
|
|
2020-04-23 21:16:17 +02:00
|
|
|
user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*})
|
|
|
|
return if user.nil?
|
|
|
|
|
|
|
|
odebug "Auditing GitLab repo"
|
|
|
|
|
|
|
|
error = SharedAudits.gitlab(user, repo)
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error error, location: cask.url.location if error
|
2020-04-23 21:16:17 +02:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_bitbucket_repository
|
2020-09-21 04:40:32 -05:00
|
|
|
return unless new_cask?
|
2020-08-13 16:17:47 +02:00
|
|
|
|
2020-04-23 21:16:17 +02:00
|
|
|
user, repo = get_repo_data(%r{https?://bitbucket\.org/([^/]+)/([^/]+)/?.*})
|
|
|
|
return if user.nil?
|
|
|
|
|
|
|
|
odebug "Auditing Bitbucket repo"
|
|
|
|
|
|
|
|
error = SharedAudits.bitbucket(user, repo)
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error error, location: cask.url.location if error
|
2020-04-23 21:16:17 +02:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_denylist
|
2021-01-26 01:16:00 -08:00
|
|
|
return unless cask.tap
|
|
|
|
return unless cask.tap.official?
|
2021-02-12 18:33:37 +05:30
|
|
|
return unless (reason = Denylist.reason(cask.token))
|
2019-09-08 09:09:37 -04:00
|
|
|
|
2020-06-06 21:10:16 +01:00
|
|
|
add_error "#{cask.token} is not allowed: #{reason}"
|
2019-09-08 09:09:37 -04:00
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2023-02-05 15:22:06 +01:00
|
|
|
def audit_reverse_migration
|
2021-01-26 01:16:00 -08:00
|
|
|
return unless new_cask?
|
|
|
|
return unless cask.tap
|
|
|
|
return unless cask.tap.official?
|
|
|
|
return unless cask.tap.tap_migrations.key?(cask.token)
|
|
|
|
|
|
|
|
add_error "#{cask.token} is listed in tap_migrations.json"
|
|
|
|
end
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
sig { void }
|
2024-01-06 11:39:04 +01:00
|
|
|
def audit_homepage_https_availability
|
|
|
|
return unless online?
|
|
|
|
return unless (homepage = cask.homepage)
|
2021-03-10 13:30:13 +01:00
|
|
|
|
2023-12-16 21:48:38 +03:00
|
|
|
user_agents = if cask.tap&.audit_exception(:simple_user_agent_for_homepage, cask.token)
|
|
|
|
["curl"]
|
|
|
|
else
|
|
|
|
[:browser, :default]
|
|
|
|
end
|
|
|
|
|
2024-01-06 11:39:04 +01:00
|
|
|
validate_url_for_https_availability(
|
|
|
|
homepage, SharedAudits::URL_TYPE_HOMEPAGE,
|
2024-03-07 16:20:20 +00:00
|
|
|
user_agents:,
|
2024-01-06 11:39:04 +01:00
|
|
|
check_content: true,
|
|
|
|
strict: strict?
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { void }
|
|
|
|
def audit_url_https_availability
|
|
|
|
return unless online?
|
|
|
|
return unless (url = cask.url)
|
|
|
|
return if url.using
|
|
|
|
|
|
|
|
validate_url_for_https_availability(
|
|
|
|
url, "binary URL",
|
|
|
|
location: cask.url.location,
|
|
|
|
user_agents: [cask.url.user_agent],
|
|
|
|
referer: cask.url&.referer
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { void }
|
|
|
|
def audit_livecheck_https_availability
|
|
|
|
return unless online?
|
|
|
|
return unless cask.livecheckable?
|
2024-01-07 15:45:24 -08:00
|
|
|
return unless (url = cask.livecheck.url)
|
|
|
|
return if url.is_a?(Symbol)
|
2024-01-06 11:39:04 +01:00
|
|
|
|
|
|
|
validate_url_for_https_availability(
|
2024-01-07 15:45:24 -08:00
|
|
|
url, "livecheck URL",
|
2024-01-06 11:39:04 +01:00
|
|
|
check_content: true
|
|
|
|
)
|
2018-10-10 21:36:02 +00:00
|
|
|
end
|
|
|
|
|
2023-08-09 14:52:30 -04:00
|
|
|
sig { void }
|
|
|
|
def audit_cask_path
|
2023-12-13 13:17:12 +00:00
|
|
|
return unless cask.tap.core_cask_tap?
|
2023-08-09 14:52:30 -04:00
|
|
|
|
2023-08-09 15:26:47 -04:00
|
|
|
expected_path = cask.tap.new_cask_path(cask.token)
|
2023-08-09 14:52:30 -04:00
|
|
|
|
2023-08-09 15:27:36 -04:00
|
|
|
return if cask.sourcefile_path.to_s.end_with?(expected_path)
|
2023-08-09 14:52:30 -04:00
|
|
|
|
|
|
|
add_error "Cask should be located in '#{expected_path}'"
|
|
|
|
end
|
|
|
|
|
2024-01-06 11:39:04 +01:00
|
|
|
sig {
|
|
|
|
params(
|
|
|
|
url_to_check: T.any(String, URL),
|
|
|
|
url_type: String,
|
|
|
|
location: T.nilable(Homebrew::SourceLocation),
|
|
|
|
options: T.untyped,
|
|
|
|
).void
|
|
|
|
}
|
|
|
|
def validate_url_for_https_availability(url_to_check, url_type, location: nil, **options)
|
2023-09-06 00:12:57 +08:00
|
|
|
problem = curl_check_http_content(url_to_check.to_s, url_type, **options)
|
2024-01-06 11:39:04 +01:00
|
|
|
exception = cask.tap&.audit_exception(:secure_connection_audit_skiplist, cask.token, url_to_check.to_s)
|
2021-10-04 19:22:30 -04:00
|
|
|
|
|
|
|
if problem
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error problem, location: location unless exception
|
2021-10-04 19:22:30 -04:00
|
|
|
elsif exception
|
2023-05-19 19:43:15 +02:00
|
|
|
add_error "#{url_to_check} is in the secure connection audit skiplist but does not need to be skipped",
|
2024-03-07 16:20:20 +00:00
|
|
|
location:
|
2021-10-04 19:22:30 -04:00
|
|
|
end
|
|
|
|
end
|
2022-09-13 10:54:05 +02:00
|
|
|
|
|
|
|
sig { params(regex: T.any(String, Regexp)).returns(T.nilable(T::Array[String])) }
|
|
|
|
def get_repo_data(regex)
|
|
|
|
return unless online?
|
|
|
|
|
|
|
|
_, user, repo = *regex.match(cask.url.to_s)
|
|
|
|
_, user, repo = *regex.match(cask.homepage) unless user
|
|
|
|
return if !user || !repo
|
|
|
|
|
|
|
|
repo.gsub!(/.git$/, "")
|
|
|
|
|
|
|
|
[user, repo]
|
|
|
|
end
|
|
|
|
|
|
|
|
sig {
|
|
|
|
params(regex: T.any(String, Regexp), valid_formats_array: T::Array[T.any(String, Regexp)]).returns(T::Boolean)
|
|
|
|
}
|
|
|
|
def bad_url_format?(regex, valid_formats_array)
|
|
|
|
return false unless cask.url.to_s.match?(regex)
|
|
|
|
|
2023-12-27 15:29:33 -08:00
|
|
|
valid_formats_array.none? { |format| cask.url.to_s.match?(format) }
|
2022-09-13 10:54:05 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
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
|
|
|
|
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def bad_osdn_url?
|
|
|
|
bad_url_format?(/osd/, [%r{\Ahttps?://([^/]+.)?dl\.osdn\.jp/}])
|
|
|
|
end
|
|
|
|
|
|
|
|
# sig { returns(String) }
|
|
|
|
def homepage
|
|
|
|
URI(cask.homepage.to_s).host
|
|
|
|
end
|
|
|
|
|
|
|
|
# sig { returns(String) }
|
|
|
|
def domain
|
|
|
|
URI(cask.url.to_s).host
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def url_match_homepage?
|
|
|
|
host = cask.url.to_s
|
|
|
|
host_uri = URI(host)
|
|
|
|
host = if host.match?(/:\d/) && host_uri.port != 80
|
|
|
|
"#{host_uri.host}:#{host_uri.port}"
|
|
|
|
else
|
|
|
|
host_uri.host
|
|
|
|
end
|
2022-09-17 16:40:00 +08:00
|
|
|
|
|
|
|
return false if homepage.blank?
|
|
|
|
|
2022-09-13 10:54:05 +02:00
|
|
|
home = homepage.downcase
|
2023-02-24 16:20:51 -08:00
|
|
|
if (split_host = T.must(host).split(".")).length >= 3
|
|
|
|
host = T.must(split_host[-2..]).join(".")
|
2022-09-13 10:54:05 +02:00
|
|
|
end
|
|
|
|
if (split_home = homepage.split(".")).length >= 3
|
|
|
|
home = split_home[-2..].join(".")
|
|
|
|
end
|
|
|
|
host == home
|
|
|
|
end
|
|
|
|
|
|
|
|
# sig { params(url: String).returns(String) }
|
|
|
|
def strip_url_scheme(url)
|
|
|
|
url.sub(%r{^[^:/]+://(www\.)?}, "")
|
|
|
|
end
|
|
|
|
|
|
|
|
# sig { returns(String) }
|
|
|
|
def url_from_verified
|
|
|
|
strip_url_scheme(cask.url.verified)
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def verified_matches_url?
|
|
|
|
url_domain, url_path = strip_url_scheme(cask.url.to_s).split("/", 2)
|
|
|
|
verified_domain, verified_path = url_from_verified.split("/", 2)
|
|
|
|
|
|
|
|
(url_domain == verified_domain || (verified_domain && url_domain&.end_with?(".#{verified_domain}"))) &&
|
|
|
|
(!verified_path || url_path&.start_with?(verified_path))
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def verified_present?
|
|
|
|
cask.url.verified.present?
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def file_url?
|
|
|
|
URI(cask.url.to_s).scheme == "file"
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def block_url_offline?
|
|
|
|
return false if online?
|
|
|
|
|
|
|
|
cask.url.from_block?
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(Tap) }
|
|
|
|
def core_tap
|
|
|
|
@core_tap ||= CoreTap.instance
|
|
|
|
end
|
|
|
|
|
|
|
|
# sig { returns(T::Array[String]) }
|
|
|
|
def core_formula_names
|
|
|
|
core_tap.formula_names
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(String) }
|
|
|
|
def core_formula_url
|
2023-08-04 16:21:31 +01:00
|
|
|
formula_path = Formulary.core_path(cask.token)
|
|
|
|
.to_s
|
|
|
|
.delete_prefix(core_tap.path.to_s)
|
2023-12-26 08:19:49 +02:00
|
|
|
"#{core_tap.default_remote}/blob/HEAD#{formula_path}"
|
2022-09-13 10:54:05 +02:00
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
end
|
|
|
|
end
|