#!/usr/bin/env ruby # # William Stearns # Copyright (c) 2013, CloudPassage, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the CloudPassage, Inc. nor the # names of its contributors may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL CLOUDPASSAGE, INC. BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED ANDON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # you may need to install the oauth2, rest-client, json, and optparse gems. # Sample command: # sudo gem install {missing_gem_name} #Version 1.6 #======== Loadable modules require 'rubygems' require 'oauth2' require 'rest-client' require 'json' require 'optparse' load 'wlslib.rb' #======== End of loadable modules #======== Initialization api_client_id = '' filter_ip_addr = '' filter_hostname = '' api_client_secret = nil api_host = nil show_bad = true show_good = false show_indeterminate = false show_suppressed = false #Timeouts manually extended to handle long setup time for large numbers #of events. Set to -1 to wait forever (although nat, proxies, and load #balancers may cut you off externally. timeout=600 open_timeout=600 issue_set = 'sca' #======== End of initialization optparse = OptionParser.new do |opts| opts.banner = "Export a SCA report in comma separated format for later spreadsheet import. Each line holds one check. Usage: sca-report-to-csv.rb [options]" opts.on("-i keyid", "--api_client_id keyid", "API Key ID (can be read only or full access)") do |keyid| api_client_id = keyid end opts.on("-a ipaddr", "IP address (interface or connecting IP address)") do |ipaddr| filter_ip_addr = ipaddr end opts.on("-n hostname", "Hostname") do |hostname| filter_hostname = hostname end opts.on("--bad", "Show checks with a bad status") do show_bad = true end opts.on("--no-bad", "Do not show checks with a bad status") do show_bad = false end opts.on("--good", "Show checks with a good status") do show_good = true end opts.on("--no-good", "Do not show checks with a good status") do show_good = false end opts.on("--indeterminate", "Show checks with a indeterminate status") do show_indeterminate = true end opts.on("--no-indeterminate", "Do not show checks with a indeterminate status") do show_indeterminate = false end opts.on("--suppressed", "Show checks with a suppressed status") do show_suppressed = true end opts.on("--no-suppressed", "Do not show checks with a suppressed status") do show_suppressed = false end opts.on("--svm", "Work with SVM results instead") do issue_set = 'svm' end opts.on_tail("-h", "--help", "Show help text") do $stderr.puts opts exit end end optparse.parse! begin File.open("/etc/halo-api-keys", "r") { |key_file_handle| key_file_line = key_file_handle.gets while (key_file_line != nil) && (api_client_secret == nil) do key_file_line.chomp! line_id, line_secret, line_host = *key_file_line.split(/\|/) #Compare, and extract secret and host and assign if line_id == api_client_id #If this line starts with the right key ID, api_client_secret = line_secret #Set the secret from the second field, api_host = line_host #And the host from the third field (set to nil if not there) end key_file_line = key_file_handle.gets end key_file_handle.close } rescue $stderr.puts "IOError caught - /etc/halo-api-keys doesn't exist or unreadable? Exiting." exit 1 end #Validate all user params if (api_client_id.to_s.length == 0) $stderr.puts "Invalid or missing api_client_id, please add a -i keyid parameter, exiting." exit 1 end if (api_host.to_s.length == 0) api_host = 'api.cloudpassage.com' end if (filter_ip_addr.to_s.length == 0) and (filter_hostname.to_s.length == 0) $stderr.puts "Command line included neither an IP Address nor Hostname parameter. Please rerun with a -a or -n command line option, exiting." exit end if (filter_ip_addr.to_s.length > 0) and (filter_hostname.to_s.length > 0) $stderr.puts "Command line included both an IP Address and a Hostname parameter. Please rerun with only one of a -a or -n command line options, exiting." exit end if (api_client_secret.to_s.length == 0) $stderr.puts "No secret found for #{api_client_id} in /etc/halo-api-keys." $stderr.puts "Please add a line to that file in the format:" $stderr.puts "#{api_client_id}|api_secret" $stderr.puts "Exiting." exit end $stderr.puts "Pulling #{issue_set} report from #{api_host} using key #{api_client_id} for #{filter_ip_addr}#{filter_hostname} ." case issue_set when 'sca' if show_bad $stderr.puts "Checks with status bad will be shown." else $stderr.puts "Checks with status bad will not be shown." end if show_good $stderr.puts "Checks with status good will be shown." else $stderr.puts "Checks with status good will not be shown." end if show_indeterminate $stderr.puts "Checks with status indeterminate will be shown." else $stderr.puts "Checks with status indeterminate will not be shown." end when 'svm' if show_suppressed $stderr.puts "Checks with status suppressed will be shown." else $stderr.puts "Checks with status suppressed will not be shown." end end #To accomodate a proxy, we need to handle both RestClient with the #following one-time statement, and also as a :proxy parameter to the #oauth2 call below. my_proxy = nil if ENV['https_proxy'].to_s.length > 0 my_proxy = ENV['https_proxy'] RestClient.proxy = my_proxy $stderr.puts "Using proxy: #{RestClient.proxy}" end token = get_auth_token(api_client_id,api_client_secret,my_proxy,api_host) if token == "" $stderr.puts "Unable to retrieve a token, exiting." exit 1 end requested_server_id = id_of_server(filter_hostname,filter_ip_addr,api_host,token,timeout,open_timeout) if (requested_server_id.to_s.length == 0) $stderr.puts "Unable to locate a server id for #{filter_hostname}#{filter_ip_addr}, exiting." exit 1 end server_issues_json = api_get("https://#{api_host}/v1/servers/#{requested_server_id}/issues",timeout,open_timeout,token) #This banner string will be the first line imported case issue_set when 'sca' puts "rule_name,check status,type,target,expected,actual,bound_process,config_key" when 'svm' puts "package_name,package_version,CVE" end server_issues_json[issue_set]['findings'].each do |one_issue| case issue_set when 'svm' if (show_bad and one_issue['status'] == "bad") one_issue['cve_entries'].each do |one_cve| if show_suppressed or (one_cve['suppressed'].to_s == 'false') puts "\"#{one_issue['package_name'].to_s.gsub('"','_')}\",\"#{one_issue['package_version'].to_s.gsub('"','_')}\",\"#{one_cve['cve_entry'].to_s.gsub('"','_')}\"" end end end when 'sca' #Use the following for _just_ rule level fields #puts "\"#{one_issue['rule_name']}\",\"#{one_issue['status']}\"" #Debug: #p one_issue #puts one_issue['details'].each do |one_detail| if (show_bad and one_detail['status'] == "bad") or (show_good and one_detail['status'] == "good") or (show_indeterminate and one_detail['status'] == "indeterminate") case one_detail['type'] when "port_white" puts "\"#{one_issue['rule_name'].to_s.gsub('"','_')}\",\"#{one_detail['status'].to_s.gsub('"','_')}\",\"#{one_detail['type'].to_s.gsub('"','_')}\",\"#{one_detail['target'].to_s.gsub('"','_')}\",\"#{one_detail['expected'].to_s.gsub('"','_')}\",\"#{one_detail['actual'].to_s.gsub('"','_')}\",\"#{one_detail['bound_process'].to_s.gsub('"','_')}\"" when "configuration" puts "\"#{one_issue['rule_name'].to_s.gsub('"','_')}\",\"#{one_detail['status'].to_s.gsub('"','_')}\",\"#{one_detail['type'].to_s.gsub('"','_')}\",\"#{one_detail['target'].to_s.gsub('"','_')}\",\"#{one_detail['expected'].to_s.gsub('"','_')}\",\"#{one_detail['actual'].to_s.gsub('"','_')}\",\"\",\"#{one_detail['config_key'].to_s.gsub('"','_')}\"" when "dir_acl", "dir_owner_gid", "dir_owner_uid", "file_acl", "file_owner_gid", "file_owner_uid", "file_presence", "file_set_uid", "file_set_gid", "group_gid_is", "group_has_users", "process_presence", "port_process", "user_has_not_logged_in", "user_has_logged_in", "user_uid_is", "user_has_groups", "windows_local_security_policy" puts "\"#{one_issue['rule_name'].to_s.gsub('"','_')}\",\"#{one_detail['status'].to_s.gsub('"','_')}\",\"#{one_detail['type'].to_s.gsub('"','_')}\",\"#{one_detail['target'].to_s.gsub('"','_')}\",\"#{one_detail['expected'].to_s.gsub('"','_')}\",\"#{one_detail['actual'].to_s.gsub('"','_')}\"" when "file_regex","user_home_setuid_files","user_home_setgid_files","user_file_presence","user_home_file_ownership","user_home_file_group_ownership","user_home_device_files","user_home_ownership","user_home_group_ownership", "user_home_files_detect_path_statements", "user_home_files_umask", "dir_sticky_bit" puts "\"#{one_issue['rule_name'].to_s.gsub('"','_')}\",\"#{one_detail['status'].to_s.gsub('"','_')}\",\"#{one_detail['type'].to_s.gsub('"','_')}\",\"#{one_detail['target'].to_s.gsub('"','_')}\"" else puts "\"#{one_issue['rule_name'].to_s.gsub('"','_')}\",\"#{one_detail['status'].to_s.gsub('"','_')}\",\"#{one_detail['type'].to_s.gsub('"','_')}\"" $stderr.puts one_detail.inspect $stderr.puts "Unrecognized check type #{one_detail['type']}, exiting." exit 1 end end end end end exit 0