require 'thor' require 'paraxial' require 'net/http' require 'uri' require 'json' require 'time' require 'yaml' require_relative 'helpers' module Paraxial class CLI < Thor desc 'scan', 'Run scan' option :github_app, type: :boolean, default: false, desc: 'Use GitHub app' option :install_id, type: :numeric, desc: 'GitHub App installation ID' option :repo_owner, type: :string, desc: 'Repository owner' option :repo_name, type: :string, desc: 'Repository name' option :pr_number, type: :numeric, desc: 'Pull request number' option :exit_code, type: :boolean, default: false, desc: 'Non-zero exit code if findings > 0' option :debug_rubocop, type: :boolean, default: false, desc: "Run rubocop in debug mode" def scan puts "[Paraxial] v#{Paraxial::VERSION} Scan starting..." case check_rubocop_configuration when :does_not_exist puts '[Paraxial] .paraxial-rubocop.yml does not exist. This file is required for the scan to run, add:' puts '.paraxial-rubocop.yml' puts 'require:' puts '- rubocop-erb' puts '' exit(1) when :found_no_erb puts '[Paraxial] .paraxial-rubocop.yml is missing rubocop-erb. To scan embedded Ruby files for security problems, add:' puts '.paraxial-rubocop.yml' puts 'require:' puts '- rubocop-erb' puts '' when :found_with_erb puts '[Paraxial] .paraxial-rubocop.yml is valid, .erb files will be scanned.' end paraxial_config = get_paraxial_configuration if paraxial_config && paraxial_config["ignore-gems"] puts '[Paraxial] .paraxial.yml configuration file found.' puts '[Paraxial] Gems ignored by dependency scan:' puts paraxial_config["ignore-gems"] elsif paraxial_config == :error puts '[Paraxial] .paraxial.yml file is not well formed and is being ignored.' paraxial_config = false elsif paraxial_config puts '[Paraxial] .paraxial.yml does not define a valid configuration. Using default configuration.' paraxial_config = false elsif paraxial_config == false puts '[Paraxial] .paraxial.yml configuration file was not found. Using default configuration.' else puts '[Paraxial] .paraxial.yml configuration is empty. Using default configuration.' paraxial_config = false end if Paraxial::Helpers.get_api_key.nil? puts '[Paraxial] Environment variable PARAXIAL_API_KEY not found' else github_app = options[:github_app] install_id = options[:install_id] repo_owner = options[:repo_owner] repo_name = options[:repo_name] pr_number = options[:pr_number] exit_code = options[:exit_code] cops = 'Paraxial,Security/Eval,Security/IoMethods,Security/JSONLoad,Security/MarshalLoad,Security/Open,Security/YAMLLoad' rubo_config = '--config .paraxial-rubocop.yml' if options[:debug_rubocop] puts '[Paraxial] rubocop debug enabled' rubocop = `rubocop --require paraxial --only #{cops} --disable-pending-cops --format json #{rubo_config} 2>/dev/null` debug_rubocop = `rubocop --debug --require paraxial --only #{cops} --disable-pending-cops #{rubo_config} 2>&1` puts debug_rubocop else rubocop = `rubocop --require paraxial --only #{cops} --disable-pending-cops --format json #{rubo_config}` end lockfile = File.read('./Gemfile.lock') api_key = ENV['PARAXIAL_API_KEY'] uri = URI.parse(Paraxial::Helpers.get_paraxial_url + '/api/ruby_scan') headers = { 'Content-Type': 'application/json' } body = { rubocop: rubocop, lockfile: lockfile, api_key: api_key, paraxial_config: paraxial_config, timestamp: Paraxial.get_timestamp } response = Net::HTTP.post(uri, body.to_json, headers) m = JSON.parse(response.body) if m['ok'].nil? puts "[Paraxial] Upload failed, check if PARAXIAL_API_KEY is valid" exit(1) end findings = m['ok']['findings'] puts puts "[Paraxial] Scan count: #{findings.length}" puts findings.each do |finding| puts finding puts end puts "[Paraxial] Scan UUID #{m['ok']['scan_uuid']}" puts "[Paraxial] Scan URL #{m['ok']['scan_url']}" github_valid = (!!github_app and !!install_id and !!repo_owner and !!repo_name and !!pr_number) if github_app and github_valid == false puts '[Paraxial] --github_app missing arguments' puts '[Paraxial] Required: --github_app, --install_id, --repo_owner, --repo_name, --pr_number' elsif github_app and github_valid # uuid_regex = /UUID\s+(\S+)/ # match = response.body.match(uuid_regex) # uuid = match[1] if match uuid = m['ok']['scan_uuid'] if uuid final_uuid = uuid.chomp('.') censored_backend_map = { 'installation_id' => install_id, 'repository_owner' => repo_owner, 'repository_name' => repo_name, 'pull_request_number' => pr_number, 'scan_uuid' => final_uuid, 'api_key' => 'REDACTED' } cbms = JSON.pretty_generate(censored_backend_map) puts "[Paraxial] GitHub hash: #{cbms}" censored_backend_map['api_key'] = api_key backend_map = censored_backend_map parax_uri = URI.parse(Paraxial::Helpers.get_paraxial_url + '/api/github_app') github_pr_url = "https://github.com/#{repo_owner}/#{repo_name}/pull/#{pr_number}" rr = Net::HTTP.post(parax_uri, backend_map.to_json, headers) puts "[Paraxial] parax_uri response: #{rr.body}" puts "[Paraxial] #{github_pr_url}" else puts '[Paraxial] No scan UUID found' end else :ok end if exit_code and (findings.length > 0) exit(1) end end end private def get_paraxial_configuration config_file = File.join(Dir.pwd, '.paraxial.yml') return false unless File.exist?(config_file) begin YAML.load_file(config_file) rescue :error end end def check_rubocop_configuration # return values: # :does_not_exist, :found_no_erb, :found_with_erb rubocop_file = File.join(Dir.pwd, '.paraxial-rubocop.yml') return :does_not_exist unless File.exist?(rubocop_file) config = YAML.load_file(rubocop_file) required_key = 'require' if config.is_a?(Hash) && config[required_key].is_a?(Array) && config[required_key].include?('rubocop-erb') :found_with_erb else :found_no_erb end end end end