#!/usr/bin/env ruby # # William Stearns # Copyright (c) 2013, William Stearns # 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. # Based on: # demo ruby cloudpassage API stuff # Tim Spencer # Thanks, Tim! # # you may need to install the oauth2, rest-client, json, public_suffix and ip gems with: # sudo gem install oauth2 rest-client json public_suffix ip #Version 0.7 #======== User-modifiable values #Maximum number of events to pull per page. Low numbers if you have an #unreliable link, but will cause more local disk writes. High numbers for #a stable link, with fewer local disk writes and a slightly faster run. #This cannot exceed 100. max_events_per_page = 100 #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 api_cache_dir = ENV['HOME']+"/api_cache" api_key_file = '/etc/halo-api-keys' default_host = 'api.cloudpassage.com' #Add the directory holding this script to the search path so we can find wlslib.rb $:.unshift File.dirname(__FILE__) #======== End of user-modifiable values #======== Functions def extract_from_events(data_events,portal_account_activity,server_account_activity) #Note, parameters parent_country and user_locations modified and returned data_events.each do |event| #=> "2011-11-30T02:58:35.012790Z" => "2011-11-30 02:58" if event['created_at'] != nil short_time_match = event['created_at'].match(/^([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])T([0-9][0-9]:[0-9][0-9]):[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9]Z/) short_time = "#{short_time_match[1]} #{short_time_match[2]}" case event['name'] when 'Local account created (Linux Only)' #No actor information $stderr.print 'sc' server_account_activity << { 'Action'=>'created', 'Account'=>event['server_account_username'], 'Timestamp'=>short_time, 'Server'=>event['server_hostname'] } when 'Local account deleted (Linux Only)' #No actor information $stderr.print 'sd' server_account_activity << { 'Action'=>'deleted', 'Account'=>event['server_account_username'], 'Timestamp'=>short_time, 'Server'=>event['server_hostname'] } when 'Halo User Invited', 'Halo User Added' #201308: Event is now 'Halo User Added', 'Invited' should no longer show up except in cache #No direct access to the account name $stderr.print 'pi' portal_account_activity << { 'Action'=>'invited', 'Account'=>event['message'].match(/Halo user (.*) was /)[1], 'Timestamp'=>short_time, 'Actor'=>event['actor_username'] } when 'Halo User Modified' #No direct access to the account name $stderr.print 'pm' begin portal_account_activity << { 'Action'=>'modified', 'Account'=>event['message'].match(/Halo user (.*) was /)[1], 'Timestamp'=>short_time, 'Actor'=>event['actor_username'] } rescue $stderr.puts event.inspect #exit 1 end when 'Halo User Deactivated' #No direct access to the account name $stderr.print 'pd' portal_account_activity << { 'Action'=>'deactivated', 'Account'=>event['message'].match(/Halo user (.*) was /)[1], 'Timestamp'=>short_time, 'Actor'=>event['actor_username'] } when 'Halo User Reactivated' $stderr.print 'pr' portal_account_activity << { 'Action'=>'reactivated', 'Account'=>event['message'].match(/Halo user (.*) was /)[1], 'Timestamp'=>short_time, 'Actor'=>event['actor_username'] } when 'Halo User Reinvited', 'Halo User Re-added' #201308: Event is now 'Halo User Re-added' $stderr.print 'pc' portal_account_activity << { 'Action'=>'reinvited', 'Account'=>event['message'].match(/Halo user (.*) was /)[1], 'Timestamp'=>short_time, 'Actor'=>event['actor_username'] } when 'Halo user activation failed' #Do we want to print these? #{"name"=>"Halo user activation failed", "server_id"=>nil, #"actor_username"=>"uuu", #"message"=>"Halo user uuu failed to activate their account from IP address () because their IP address is not authorized", #"actor_ip_address"=>nil, "actor_country"=>nil, #"created_at"=>"2013-04-11T17:40:38.874973Z", "critical"=>false} when 'Halo user account locked' # #Do we want to print these? #{"message"=>"Halo user uuu was locked due to excessive failed login attempts.", #"name"=>"Halo user account locked", #"actor_country"=>"USA", "actor_username"=>"uuu", #"actor_ip_address"=>"IP.address", #"created_at"=>"2013-04-11T06:52:55.220101Z", "critical"=>true, #"server_id"=>nil} when 'Halo user account unlocked' # #Do we want to print these? #{"actor_username"=>"uuu", "actor_country"=>"USA", #"message"=>"Halo user uuu was unlocked.", #"created_at"=>"2013-04-11T07:06:36.576038Z", "critical"=>false, #"name"=>"Halo user account unlocked", #"actor_ip_address"=>"IP.address", "server_id"=>nil} when 'API Key Created', 'API Key Deleted', 'API Key Modified', 'API Secret Key Viewed' when 'Authorized IPs modified' when 'Automatic file integrity scan schedule modified', 'Automatic file integrity scanning disabled', 'Automatic file integrity scanning enabled' when 'Configuration policy assigned', 'Configuration policy created', 'Configuration policy deleted', 'Configuration policy exported', 'Configuration policy imported', 'Configuration policy modified', 'Configuration policy unassigned' when 'Configuration rule matched' when 'Daemon compromised', 'Daemon version changed' when 'File Integrity baseline', 'File Integrity baseline deleted', 'File Integrity baseline expired', 'File Integrity baseline failed' when 'File Integrity exception created', 'File Integrity exception deleted', 'File Integrity exception expired' when 'File Integrity object added', 'File Integrity object missing', 'File Integrity object signature changed' when 'File Integrity policy assigned', 'File Integrity policy created', 'File Integrity policy deleted', 'File Integrity policy exported', 'File Integrity policy imported', 'File Integrity policy modified', 'File Integrity policy unassigned' when 'File Integrity re-baseline' when 'File Integrity scan requested' when 'GhostPorts login failure', 'GhostPorts login success', 'GhostPorts provisioning', 'GhostPorts session close' when 'Halo firewall policy assigned', 'Halo firewall policy created', 'Halo firewall policy deleted', 'Halo firewall policy modified', 'Halo firewall policy unassigned' when 'Halo login failure', 'Halo login success', 'Halo logout' when 'Halo password changed', 'Halo password recovery requested', 'Halo password recovery success', 'Halo password recovery request failed' when 'Halo session timeout' when 'Multiple root accounts detected (Linux Only)' when 'Network service added', 'Network service deleted', 'Network service modified' when 'New server' when 'Password configuration settings modified' when 'SMS phone number verified' when 'Server firewall restore requested' when 'Server IP address changed', 'Server deleted', 'Server firewall modified', 'Server missing', 'Server moved to another group', 'Server restarted', 'Server retired', 'Server shutdown', 'Server un-retired' else $stderr.puts "#{event['name']} unhandled, exiting." $stderr.puts event.inspect #exit 1 end end end end def write_account_report(portal_account_activity,server_account_activity,starting_date,api_client_ids) puts "Halo portal and server account timeline" puts "

These are the Portal and Server-local account changes from the Portal since #{starting_date}," print "retrieved with key" print "s" if api_client_ids.length > 1 print ": " print api_client_ids.join(', ') puts " . All times are in Zulu.

" puts "

Portal account timeline

" puts "" puts "" portal_account_activity.sort_by{|el| el['Timestamp'] }.each do |one_activity| puts "" end puts "
TimeAccount actionActor
#{one_activity['Timestamp']}#{one_activity['Account']} #{one_activity['Action']}#{one_activity['Actor']}
" puts "

Server-local account timeline

" puts "" puts "" server_account_activity.sort_by{|el| el['Timestamp'] }.each do |one_activity| puts "" end puts "
TimeServerAccount action
#{one_activity['Timestamp']}#{one_activity['Server']}#{one_activity['Account']} #{one_activity['Action']}
" puts "Report generated: #{Time.now}" puts "" end #======== End of Functions #======== Loadable modules require 'rubygems' require 'optparse' require 'oauth2' require 'rest-client' require 'json' require 'date' load 'wlslib.rb' #======== End of loadable modules #======== Initialization api_client_ids = [ ] api_secrets = { } api_hosts = { } my_proxy = nil starting_date = '1970-01-01' etc_hosts = [ ] portal_account_activity = [ ] #Array of hashes server_account_activity = [ ] #Array of hashes default_key = "" #======== End of initialization #======== Parse command line options optparse = OptionParser.new do |opts| opts.banner = "Identify IP addresses of both successful and failed portal logins for each user. Usage: watn.rb [options]" opts.on("-i keyid", "--api_client_id keyid", "API Key ID (can be read only or full access). If no key specified, use first key. If ALL , use all keys.") do |keyid| api_client_ids << keyid unless api_client_ids.include?(keyid) end opts.on("-s date", "Starting date of events to process(YYYY-MM-DD format)") do |user_date| starting_date = user_date end opts.on_tail("-h", "--help", "Show help text") do $stderr.puts opts exit end end optparse.parse! #======== Load api keys default_key = load_api_keys(api_key_file,api_secrets,api_hosts,default_host) if default_key == "" $stderr.puts "Unable to load any keys from #{api_key_file}, exiting." exit 1 end #======== Validate all user params #FIXME - validate starting_date if api_client_ids.length == 0 $stderr.puts "No key requested on command line; using the first valid key in #{api_key_file}, #{default_key}." api_client_ids << default_key elsif (api_client_ids.include?('ALL')) or (api_client_ids.include?('All')) or (api_client_ids.include?('all')) $stderr.puts "\"ALL\" requested; using all available keys in #{api_key_file}: #{api_secrets.keys.join(',')}" api_client_ids = api_secrets.keys.sort end if ! (1..100).include?(max_events_per_page) $stderr.puts "Invalid setting for max_events_per_page; must be between 1 and 100. Exiting." exit 1 end #Test that api_cache_dir exists (FIXME - later test that it is writeable) unless File.directory?(api_cache_dir) $stderr.puts "'#{api_cache_dir}' is not a directory. Please create it or edit api_cache_dir in this script. Exiting." exit 1 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. if ENV['https_proxy'].to_s.length > 0 my_proxy = ENV['https_proxy'] RestClient.proxy = my_proxy $stderr.puts "Using proxy: #{RestClient.proxy}" end #Pull in event data for each api key id api_client_ids.each do |one_client_id| if (api_secrets[one_client_id].to_s.length == 0) $stderr.puts "Invalid or missing api_client_secret for key id #{one_client_id}, skipping this key." $stderr.puts "The mode 600 file #{api_key_file} should contain one line per key ID/secret like:" $stderr.puts "myid1|mysecret1" $stderr.puts "myid2|mysecret2[|optional apihost:port]" else $stderr.puts "Pulling events from #{api_hosts[one_client_id]} using key #{one_client_id}, #{max_events_per_page} events per page." #If this script runs a long time, we'll need to get a new session key if #we're within a minute of the timeout. Remember the timeout for later. #FIXME - get timeout from response instead of hardcoding revalidate_stamp = Time.now.to_i + 900 token = get_auth_token(one_client_id,api_secrets[one_client_id],my_proxy,api_hosts[one_client_id]) if token == "" $stderr.puts "Unable to retrieve a token, skipping account #{one_client_id}." else #Find the date of the first event after the user defined starting date first_event = cached_api_get("https://#{api_hosts[one_client_id]}/v1/events?per_page=1&page=1&since=#{starting_date}",timeout,open_timeout,token,one_client_id,api_cache_dir) #$stderr.puts first_event.inspect if first_event['count'] == 0 $stderr.puts "Portal account #{one_client_id} does not appear to have events, skipping." else first_event_date = first_event['events'][0]['created_at'].match(/^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])/) #=> "2011-11-30T02:58:35.012790Z" => "2011-11-30" date_range = Date.new(first_event_date[1].to_i, first_event_date[2].to_i, first_event_date[3].to_i)..(Date.today + 1) date_range.each do |day| #puts "#{day.year} #{day.month}, #{day.day} #{(day + 1).year} #{(day + 1).month}, #{(day + 1).day}" more_api_params = "&since=#{day.year}-#{day.month}-#{day.day}&until=#{(day + 1).year}-#{(day + 1).month}-#{(day + 1).day}&type=server_account_created,server_account_deleted,halo_user_invited,halo_user_modified,halo_user_deactivated,halo_user_reactivated,halo_user_reinvited,activation_link_failed,halo_user_locked,halo_user_unlocked" $stderr.print " #{day.year}-#{day.month}-#{day.day}" STDERR.flush page = 1 #Get the first page of events from the Halo grid. data = cached_api_get("https://#{api_hosts[one_client_id]}/v1/events?per_page=#{max_events_per_page}&page=#{page}#{more_api_params}",timeout,open_timeout,token,one_client_id,api_cache_dir) while ( data['events'].length > 0 ) do $stderr.print "." STDERR.flush #user_locations and parent_country modified and returned as params extract_from_events(data['events'],portal_account_activity,server_account_activity) #If this script runs a long time, we'll need to get a new session key if #we're within a minute of the timeout if ( Time.now.to_i > ( revalidate_stamp - 60 ) ) #FIXME - get timeout from response instead of hardcoding revalidate_stamp = Time.now.to_i + 900 token = get_auth_token(one_client_id,api_secrets[one_client_id],my_proxy,api_hosts[one_client_id]) end #Get the next page of events from the Halo grid. page += 1 data = cached_api_get("https://#{api_hosts[one_client_id]}/v1/events?per_page=#{max_events_per_page}&page=#{page}#{more_api_params}",timeout,open_timeout,token,one_client_id,api_cache_dir) end end end end $stderr.puts STDERR.flush end end write_account_report(portal_account_activity,server_account_activity,starting_date,api_client_ids) $stderr.puts " Complete." exit 0