#!/usr/bin/env ruby # frozen_string_literal: false require 'cgi' require 'optparse' require 'pwn' require 'yaml' opts = {} OptionParser.new do |options| options.banner = "USAGE: #{$PROGRAM_NAME} [opts] " options.on('-cCONFIG', '--config=CONFG', '') do |g| opts[:config] = g end options.on('-pID', '--parent-group-id=ID', '') do |p| opts[:parent_group_id] = p end options.on('-sFILE', '--scan=FILE', '') do |f| opts[:target_file] = f end options.on('-rPATH', '--report=PATH', '') do |r| opts[:report_path] = r end options.on('-q', '--queue-timeout', '') do |q| opts[:queue_timeout] = q end options.on('-a', '--scan-attempts', '') do |a| opts[:scan_attempts] = a end options.on('-S', '--sleep-between-scan-attempts', '') do |s| opts[:sleep_between_scan_attempts] = s end options.on('-R', '--report-only', '') do |o| opts[:report_only] = o end options.on('-tTYPE', '--report-type=TYPE', '') do |t| opts[:report_type] = t end options.on('-vVERSION', '--version=VERSION', '') do |v| opts[:version] = v end end.parse! if opts.empty? puts `#{$PROGRAM_NAME} --help` exit 1 end abort_total = 1 begin pwn_provider = 'ruby-gem' pwn_provider = ENV.fetch('PWN_PROVIDER') if ENV.keys.any? { |s| s == 'PWN_PROVIDER' } config = opts[:config] raise "ERROR: BDBA YAML Config File Not Found: #{config}" unless File.exist?(config) yaml_config = YAML.load_file(config, symbolize_names: true) token = yaml_config[:token] raise "ERROR: BDBA Token Not Found: #{token}" if token.nil? parent_group_id = opts[:parent_group_id] raise "ERROR: BDBA Parent Group ID Not Provided: #{parent_group_id}" if parent_group_id.nil? target_file = opts[:target_file] raise "ERROR: BDBA Target File Not Found: #{target_file}" unless File.exist?(target_file) report_path = opts[:report_path] raise "ERROR: BDBA Report Path Not Provided: #{report_path}" if report_path.nil? queue_timeout = opts[:queue_timeout] ||= 5_400 scan_attempts = opts[:scan_attempts] ||= 3 sleep_between_scan_attempts = opts[:sleep_between_scan_attempts] ||= 60 report_only = opts[:report_only] ||= false report_type_str = opts[:report_type] ||= 'csv_vulns' report_type = report_type_str.to_s.to_sym version = opts[:version] unless report_only puts "Uploading/Scanning: #{target_file}" PWN::Plugins::BlackDuckBinaryAnalysis.upload_file( token: token, file: target_file, group_id: parent_group_id, version: version ) puts "Scan Attempt #{abort_total} of #{scan_attempts}..." end scan_progress_resp = {} scan_progress_busy_duration = 0 loop do scan_progress_resp = PWN::Plugins::BlackDuckBinaryAnalysis.get_apps_by_group( token: token, group_id: parent_group_id ) # Break out of infinite loop if status is anything other than 'B' (i.e. 'Busy') # Possible status other than 'B' is: # 'R' (i.e. 'Ready') or # 'F' (i.e. 'Fail') break if scan_progress_resp[:products].none? { |p| p[:status] == 'B' } || report_only # Cancel queued scan if it's been queued for more than 90 minutes if scan_progress_busy_duration > queue_timeout.to_i puts "Scan Queued for More than #{queue_timeout} Seconds." scan_progress_resp[:products].select { |p| p[:status] == 'B' }.each do |p| puts "Abort Queued Scan: #{p[:name]}" PWN::Plugins::BlackDuckBinaryAnalysis.abort_product_scan( token: token, product_id: p[:product_id] ) end raise IO::TimeoutError, "ERROR: BDBA Scan Aborted: #{target_file}" end 10.times do print '.' sleep 1 end scan_progress_busy_duration += 10 end raise 'ERROR: BDBA Scan Failed - Check BDBA Logs for More Info...' if scan_progress_resp[:products].any? { |p| p[:status] == 'F' } # Account for rare race condition scenario where get_apps_by_group may need to be called # multiple times to find the product find_product = nil find_product_attempts = scan_attempts print 'Looking for Product in Apps by Group...' loop do # target_basename = CGI.unescape_uri_component(File.basename(target_file)) # ^ Synopsis unescapes it for us. target_basename = File.basename(target_file) find_product = scan_progress_resp[:products].find { |p| p[:name] == target_basename } break unless find_product.nil? find_product_attempts += 1 raise "ERROR: Cannot Find Product in Apps by Group:\n#{scan_progress_resp}" if find_product_attempts >= scan_attempts 10.times do print '.' sleep 1 end scan_progress_resp = PWN::Plugins::BlackDuckBinaryAnalysis.get_apps_by_group( token: token, group_id: parent_group_id ) end puts 'complete.' product_id = find_product[:product_id] scan_report_resp = PWN::Plugins::BlackDuckBinaryAnalysis.generate_product_report( token: token, product_id: product_id, type: report_type, output_path: report_path ) puts "\nReport Saved to: #{report_path}" rescue IO::TimeoutError, RestClient::BadGateway, RestClient::BadRequest, RestClient::Conflict, RestClient::Exceptions::OpenTimeout, RestClient::Forbidden, RestClient::GatewayTimeout, RestClient::InternalServerError, RestClient::ResourceNotFound, RestClient::ServiceUnavailable, RestClient::Unauthorized abort_total += 1 if abort_total <= scan_attempts.to_i puts 'Sleeping for 60 Seconds and Retrying...' sleep sleep_between_scan_attempts.to_i retry end puts 'Scan Attempts Reached - Goodbye.' exit 1 rescue SystemExit, Interrupt puts "\nGoodbye." rescue StandardError => e raise e end