require 'open-uri' require_relative '../client/whitelist_resolver' POD_NAME_REGEX = /^([^\/]+)(?:\/.*)*$/ POD_BASE_REGEX_POSITION = 0 DEFAULT_WHITELIST_URL = "https://raw.githubusercontent.com/mercadolibre/mobile-dependencies_whitelist/master/ios-whitelist.json" module Pod class Command class Whitelist < Command self.summary = "Validate Podspec's dependencies against a whitelist of pods." self.description = <<-DESC Validate Podspec's dependencies against a whitelist of pods. DESC self.arguments = [ CLAide::Argument.new('config', false), CLAide::Argument.new('podspec', false), CLAide::Argument.new('fail-on-error', false), CLAide::Argument.new('outfile', false) ] def self.options [ ['--config=CONFIG', 'Config file or URL for the blacklist'], ['--podspec=PODSPEC', 'Podspec file to be lint'], ['--fail-on-error', 'Raise an exception in case of error'], ['--outfile=PATH', 'Output the linter results to a file'] ].concat(super) end def initialize(argv) @whitelist_url = argv ? argv.option('config', DEFAULT_WHITELIST_URL) : DEFAULT_WHITELIST_URL @pospec_path = argv ? argv.option('podspec') : nil @fail_on_error = argv ? argv.flag?('fail-on-error') : false @outfile = argv ? argv.option('outfile') : nil @failure = false super end def validate! help! "A whitelist file or URL is needed." unless @whitelist_url end def run prepare_outfile whitelist = WhitelistResolver.instance.get_whitelist(@whitelist_url) specifications = get_podspec_specifications if specifications.empty? UI.puts "No Podspec found".yellow return end specifications.map do |specification| validate_dependencies(JSON.parse(specification.to_json), whitelist) end show_result_message end def show_result_message return unless @failure message = "Please check your dependencies.\nYou can see the allowed dependencies at #{@whitelist_url}" show_error_message(message) if @fail_on_error raise Informative.new() end end # Checks the dependencies the project contains are in the whitelist def validate_dependencies(podspec, whitelist, parentName = nil) pod_name = parentName ? "#{parentName}/#{podspec['name']}" : podspec['name'] UI.puts "Verifying dependencies in #{pod_name}".green dependencies = podspec["dependencies"] ? podspec["dependencies"] : [] not_allowed = [] dependencies.each do |name, versions| # Skip subspec dependency next if parentName && name.start_with?("#{parentName}/") if versions.length != 1 not_allowed.push("#{name} (#{versions.join(", ")}) Reason: A specific version must be defined for every dependency (just one). " + "Suggestion: find this dependency in your Podspec and add the version listed in the whitelist.") next end allowedDependency = whitelist.select { |item| name.start_with?(item.name.match(POD_NAME_REGEX).captures[POD_BASE_REGEX_POSITION]) && (!item.version || versions.grep(/#{item.version}/).any?) && (item.target == 'production') } # Checks if any of the allowed dependencies are expired, if so, fail with error allowedDependency.each { |dependency| if dependency.expire? not_allowed.push("#{name} Reason: Expired version. Please check the whitelist.") end } if allowedDependency.empty? not_allowed.push("#{name} (#{versions.join(", ")}) Reason: Specified version hasn't match any whitelisted version or Pod name is not valid") next end end if not_allowed.any? severity = @fail_on_error ? "Error" : "Warning" show_error_message(" #{severity}: Found dependencies not allowed:") not_allowed.each {|dependency| show_error_message(" - #{dependency}")} @failure = true else UI.puts " OK".green end # Validate subspecs dependencies if podspec["subspecs"] podspec["subspecs"].each do |subspec| validate_dependencies(subspec, whitelist, pod_name) end end end def get_podspec_specifications if @pospec_path return [Pod::Specification.from_file(@pospec_path)] end # Search .podspec in current directory podspecs = Dir.glob("./*.podspec") if podspecs.count == 0 # Search .podspec in parent directory. # Some projects has Podfile into a subdirectory ("Example"), and run "pod install" from there. podspecs = Dir.glob("../*.podspec") end return podspecs.map { |path| Pod::Specification.from_file(path) } end def show_error_message(message) unless @outfile == nil IO.write(@outfile, "#{message}\n", mode: 'a') end if @fail_on_error UI.puts message.red else UI.puts message.yellow end end def prepare_outfile if @outfile == nil return end if File.exist?(@outfile) FileUtils.rm(@outfile) elsif File.dirname(@outfile) FileUtils.mkdir_p(File.dirname(@outfile)) end end end end end