diff options
Diffstat (limited to 'Library/Homebrew/dev-cmd/audit.rb')
| -rw-r--r-- | Library/Homebrew/dev-cmd/audit.rb | 173 |
1 files changed, 95 insertions, 78 deletions
diff --git a/Library/Homebrew/dev-cmd/audit.rb b/Library/Homebrew/dev-cmd/audit.rb index f8236e6a6..b58672043 100644 --- a/Library/Homebrew/dev-cmd/audit.rb +++ b/Library/Homebrew/dev-cmd/audit.rb @@ -53,7 +53,8 @@ module Homebrew module_function def audit - Homebrew.inject_dump_stats!(FormulaAuditor, /^audit_/) if ARGV.switch? "D" + inject_dump_stats!(FormulaAuditor, /^audit_/) if ARGV.switch? "D" + Homebrew.auditing = true formula_count = 0 problem_count = 0 @@ -201,19 +202,24 @@ class FormulaAuditor @specs = %w[stable devel head].map { |s| formula.send(s) }.compact end - def self.check_http_content(url, name, user_agents: [:default]) + def self.check_http_content(url, user_agents: [:default], check_content: false, strict: false, require_http: false) return unless url.start_with? "http" details = nil user_agent = nil - hash_needed = url.start_with?("http:") && name != "curl" + hash_needed = url.start_with?("http:") && !require_http user_agents.each do |ua| details = http_content_headers_and_checksum(url, hash_needed: hash_needed, user_agent: ua) user_agent = ua break if details[:status].to_s.start_with?("2") end - return "The URL #{url} is not reachable" unless details[:status] + unless details[:status] + # Hack around https://github.com/Homebrew/brew/issues/3199 + return if MacOS.version == :el_capitan + return "The URL #{url} is not reachable" + end + unless details[:status].start_with? "2" return "The URL #{url} is not reachable (HTTP status code #{details[:status]})" end @@ -236,18 +242,40 @@ class FormulaAuditor details[:content_length] == secure_details[:content_length] file_match = details[:file_hash] == secure_details[:file_hash] - return if !etag_match && !content_length_match && !file_match - "The URL #{url} could use HTTPS rather than HTTP" + if etag_match || content_length_match || file_match + return "The URL #{url} should use HTTPS rather than HTTP" + end + + return unless check_content + + no_protocol_file_contents = %r{https?:\\?/\\?/} + details[:file] = details[:file].gsub(no_protocol_file_contents, "/") + secure_details[:file] = secure_details[:file].gsub(no_protocol_file_contents, "/") + + # Check for the same content after removing all protocols + if details[:file] == secure_details[:file] + return "The URL #{url} should use HTTPS rather than HTTP" + end + + return unless strict + + # Same size, different content after normalization + # (typical causes: Generated ID, Timestamp, Unix time) + if details[:file].length == secure_details[:file].length + return "The URL #{url} may be able to use HTTPS rather than HTTP. Please verify it in a browser." + end + + lenratio = (100 * secure_details[:file].length / details[:file].length).to_i + return unless (90..110).cover?(lenratio) + "The URL #{url} may be able to use HTTPS rather than HTTP. Please verify it in a browser." end def self.http_content_headers_and_checksum(url, hash_needed: false, user_agent: :default) max_time = hash_needed ? "600" : "25" - args = curl_args( - extra_args: ["--connect-timeout", "15", "--include", "--max-time", max_time, url], - show_output: true, - user_agent: user_agent, + output, = curl_output( + "--connect-timeout", "15", "--include", "--max-time", max_time, "--location", url, + user_agent: user_agent ) - output = Open3.popen3(*args) { |_, stdout, _, _| stdout.read } status_code = :unknown while status_code == :unknown || status_code.to_s.start_with?("3") @@ -262,6 +290,7 @@ class FormulaAuditor etag: headers[%r{ETag: ([wW]\/)?"(([^"]|\\")*)"}, 2], content_length: headers[/Content-Length: (\d+)/, 1], file_hash: output_hash, + file: output, } end @@ -329,7 +358,8 @@ class FormulaAuditor end valid_alias_names = [alias_name_major, alias_name_major_minor] - if formula.tap && !formula.tap.core_tap? + unless formula.tap&.core_tap? + versioned_aliases.map! { |a| "#{formula.tap}/#{a}" } valid_alias_names.map! { |a| "#{formula.tap}/#{a}" } end @@ -357,31 +387,17 @@ class FormulaAuditor end end - def audit_class - if @strict - unless formula.test_defined? - problem "A `test do` test block should be added" - end - end - - classes = %w[GithubGistFormula ScriptFileFormula AmazonWebServicesFormula] - klass = classes.find do |c| - Object.const_defined?(c) && formula.class < Object.const_get(c) - end - - problem "#{klass} is deprecated, use Formula instead" if klass + def self.aliases + # core aliases + tap alias names + tap alias full name + @aliases ||= Formula.aliases + Formula.tap_aliases end - # core aliases + tap alias names + tap alias full name - @@aliases ||= Formula.aliases + Formula.tap_aliases - def audit_formula_name return unless @strict # skip for non-official taps return if formula.tap.nil? || !formula.tap.official? name = formula.name - full_name = formula.full_name if Homebrew::MissingFormula.blacklisted_reason(name) problem "'#{name}' is blacklisted." @@ -397,33 +413,10 @@ class FormulaAuditor return end - if !formula.core_formula? && Formula.core_names.include?(name) - problem "Formula name conflicts with existing core formula." - return - end - - @@local_official_taps_name_map ||= Tap.select(&:official?).flat_map(&:formula_names) - .each_with_object({}) do |tap_formula_full_name, name_map| - tap_formula_name = tap_formula_full_name.split("/").last - name_map[tap_formula_name] ||= [] - name_map[tap_formula_name] << tap_formula_full_name - name_map - end + return if formula.core_formula? + return unless Formula.core_names.include?(name) - same_name_tap_formulae = @@local_official_taps_name_map[name] || [] - - if @online - Homebrew.search_taps(name).each do |tap_formula_full_name| - tap_formula_name = tap_formula_full_name.split("/").last - next if tap_formula_name != name - same_name_tap_formulae << tap_formula_full_name - end - end - - same_name_tap_formulae.delete(full_name) - - return if same_name_tap_formulae.empty? - problem "Formula name conflicts with #{same_name_tap_formulae.join ", "}" + problem "Formula name conflicts with existing core formula." end def audit_deps @@ -451,7 +444,7 @@ class FormulaAuditor problem "Dependency '#{dep.name}' was renamed; use new name '#{dep_f.name}'." end - if @@aliases.include?(dep.name) && + if self.class.aliases.include?(dep.name) && (dep_f.core_formula? || !dep_f.versioned_formula?) problem "Dependency '#{dep.name}' is an alias; use the canonical name '#{dep.to_formula.full_name}'." end @@ -462,16 +455,16 @@ class FormulaAuditor problem "Dependency '#{dep.name}' may be unnecessary as it is provided by macOS; try to build this formula without it." end - dep.options.reject do |opt| - next true if dep_f.option_defined?(opt) - dep_f.requirements.detect do |r| + dep.options.each do |opt| + next if dep_f.option_defined?(opt) + next if dep_f.requirements.detect do |r| if r.recommended? opt.name == "with-#{r.name}" elsif r.optional? opt.name == "without-#{r.name}" end end - end.each do |opt| + problem "Dependency #{dep} does not define option #{opt.name.inspect}" end @@ -564,10 +557,11 @@ class FormulaAuditor return unless @online - return unless DevelopmentTools.curl_handles_most_https_homepages? + return unless DevelopmentTools.curl_handles_most_https_certificates? if http_content_problem = FormulaAuditor.check_http_content(homepage, - formula.name, - user_agents: [:browser, :default]) + user_agents: [:browser, :default], + check_content: true, + strict: @strict) problem http_content_problem end end @@ -598,7 +592,8 @@ class FormulaAuditor return if metadata.nil? problem "GitHub fork (not canonical repository)" if metadata["fork"] - if (metadata["forks_count"] < 20) && (metadata["subscribers_count"] < 20) && + if formula&.tap&.core_tap? && + (metadata["forks_count"] < 20) && (metadata["subscribers_count"] < 20) && (metadata["stargazers_count"] < 50) problem "GitHub repository not notable enough (<20 forks, <20 watchers and <50 stars)" end @@ -617,13 +612,14 @@ class FormulaAuditor end %w[Stable Devel HEAD].each do |name| - next unless spec = formula.send(name.downcase) + spec_name = name.downcase.to_sym + next unless spec = formula.send(spec_name) - ra = ResourceAuditor.new(spec, online: @online, strict: @strict).audit + ra = ResourceAuditor.new(spec, spec_name, online: @online, strict: @strict).audit problems.concat ra.problems.map { |problem| "#{name}: #{problem}" } spec.resources.each_value do |resource| - ra = ResourceAuditor.new(resource, online: @online, strict: @strict).audit + ra = ResourceAuditor.new(resource, spec_name, online: @online, strict: @strict).audit problems.concat ra.problems.map { |problem| "#{name} resource #{resource.name.inspect}: #{problem}" } @@ -688,7 +684,7 @@ class FormulaAuditor end stable = formula.stable - case stable && stable.url + case stable&.url when /[\d\._-](alpha|beta|rc\d)/ matched = Regexp.last_match(1) version_prefix = stable.version.to_s.sub(/\d+$/, "") @@ -855,7 +851,7 @@ class FormulaAuditor def audit_reverse_migration # Only enforce for new formula being re-added to core and official taps return unless @strict - return unless formula.tap && formula.tap.official? + return unless formula.tap&.official? return unless formula.tap.tap_migrations.key?(formula.name) problem <<-EOS.undent @@ -918,10 +914,10 @@ class FormulaAuditor end class ResourceAuditor - attr_reader :problems - attr_reader :version, :checksum, :using, :specs, :url, :mirrors, :name + attr_reader :name, :version, :checksum, :url, :mirrors, :using, :specs, :owner + attr_reader :spec_name, :problems - def initialize(resource, options = {}) + def initialize(resource, spec_name, options = {}) @name = resource.name @version = resource.version @checksum = resource.checksum @@ -929,9 +925,11 @@ class ResourceAuditor @mirrors = resource.mirrors @using = resource.using @specs = resource.specs - @online = options[:online] - @strict = options[:strict] - @problems = [] + @owner = resource.owner + @spec_name = spec_name + @online = options[:online] + @strict = options[:strict] + @problems = [] end def audit @@ -1005,11 +1003,29 @@ class ResourceAuditor problem "Redundant :using value in URL" end + def self.curl_openssl_and_deps + @curl_openssl_and_deps ||= begin + formulae_names = ["curl", "openssl"] + formulae_names += formulae_names.flat_map do |f| + Formula[f].recursive_dependencies.map(&:name) + end + formulae_names.uniq + rescue FormulaUnavailableError + [] + end + end + def audit_urls urls = [url] + mirrors - if name == "curl" && !urls.find { |u| u.start_with?("http://") } - problem "should always include at least one HTTP url" + curl_openssl_or_deps = ResourceAuditor.curl_openssl_and_deps.include?(owner.name) + + if spec_name == :stable && curl_openssl_or_deps + problem "should not use xz tarballs" if url.end_with?(".xz") + + unless urls.find { |u| u.start_with?("http://") } + problem "should always include at least one HTTP mirror" + end end return unless @online @@ -1021,7 +1037,7 @@ class ResourceAuditor # A `brew mirror`'ed URL is usually not yet reachable at the time of # pull request. next if url =~ %r{^https://dl.bintray.com/homebrew/mirror/} - if http_content_problem = FormulaAuditor.check_http_content(url, name) + if http_content_problem = FormulaAuditor.check_http_content(url, require_http: curl_openssl_or_deps) problem http_content_problem end elsif strategy <= GitDownloadStrategy @@ -1030,6 +1046,7 @@ class ResourceAuditor end elsif strategy <= SubversionDownloadStrategy next unless DevelopmentTools.subversion_handles_most_https_certificates? + next unless Utils.svn_available? unless Utils.svn_remote_exists url problem "The URL #{url} is not a valid svn URL" end |
