├── README.md ├── Nexpose-Lieberman-Integration ├── Integration Guide Lieberman.pdf ├── nexpose_integration.rb └── lieberman_integration.rb ├── conf ├── vulnotify.yaml └── nexpose.yaml ├── templateVulnList.rb ├── delete_devices_from_group.rb ├── regex_dag.rb ├── nexposeBackup.rb ├── report_distlist.rb ├── listEngines.rb ├── dhcp_heal.rb ├── scanAssetGroup.rb ├── scan_asset_group.rb ├── export_running_log.rb ├── stopScansForEngine.rb ├── moveEngineToPool.rb ├── openPortQuery.rb ├── add_global_exclusion.rb ├── discoveryCount.rb ├── search_ip.rb ├── powershell ├── alter_credential.ps1 ├── example_site_get.ps1 ├── alter_credential_build.ps1 ├── create_asset_group.ps1 └── example_start_scan_post.ps1 ├── nexposeListBackups.rb ├── create_asset_group.rb ├── addToAssetGroup.rb ├── createAssetGroup.rb ├── deleteStaleAssets.rb ├── stopPausedScans.rb ├── dbMaint.rb ├── discoveryCountCSV.rb ├── updateEmailAlerts.rb ├── vulnIDQuery.rb ├── assetGroupQuery.rb ├── massMaxDurationMod.rb ├── createSyslogAlerts.rb ├── createEmailAlerts.rb ├── calGen.rb ├── scan_mailer.rb ├── scanCleanup.rb ├── scan_by_iplist.rb ├── adhocScanGen.rb ├── smartCleanup.rb ├── systemCheck.rb ├── vulnReporter.rb └── logtime.py /README.md: -------------------------------------------------------------------------------- 1 | nexpose 2 | ======= 3 | 4 | generic scripts for managing nexpose 5 | 6 | See: https://github.com/BrianWGray/nexpose/wiki -------------------------------------------------------------------------------- /Nexpose-Lieberman-Integration/Integration Guide Lieberman.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianWGray/nexpose/HEAD/Nexpose-Lieberman-Integration/Integration Guide Lieberman.pdf -------------------------------------------------------------------------------- /conf/vulnotify.yaml: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # 3 | # Vulnerabilities to take action on and the action to take 4 | # 5 | ############################################################ 6 | 7 | --- 8 | - vuln: "cmty-http-ricoh-no-password" 9 | vulnId: "cmty-http-ricoh-no-password" 10 | reporter_types: [email] 11 | 12 | - vuln: "apache-tomcat-default-password" 13 | vulnId: "apache-tomcat-default-password" 14 | reporter_types: [email] 15 | 16 | - vuln: "windows-hotfix-ms08-067" 17 | vulnId: "windows-hotfix-ms08-067" 18 | reporter_types: [email] 19 | 20 | - vuln: "http-openssl-cve-2014-0160" 21 | vulnId: "http-openssl-cve-2014-0160" 22 | reporter_types: [email] 23 | 24 | - vuln: "cmty-ssh-default-account-" 25 | vulnId: "cmty-ssh-default-account-%" 26 | reporter_types: [email] 27 | 28 | - vuln: "cmty-telnet-default-account-" 29 | vulnId: "cmty-telnet-default-account-%" 30 | reporter_types: [email] -------------------------------------------------------------------------------- /conf/nexpose.yaml: -------------------------------------------------------------------------------- 1 | # Nexpose-Client Script Configurations 2 | 3 | 4 | hostname: nexpose.example.com 5 | username: apiuser 6 | passwordkey: "apiuserpassword" 7 | port: "3780" 8 | logserver: "localhost" 9 | logport: 514 10 | alertFail: 1 11 | alertPause: 1 12 | alertResume: 1 13 | alertStart: 1 14 | alertStop: 1 15 | alertConfirmed: 1 16 | alertPotential: 1 17 | alertSeverity: 1 18 | alertUnconfirmed: 1 19 | adhocscantemplate: "full-audit" 20 | adhocscanengine: 2 21 | netconfigoutfile: "./conf/netconfig.yaml" 22 | servicetimeout: 600 23 | cleanupqueue: 5 24 | cleanupwaittime: 60 25 | nexposeajaxtimeout: 1200000000 26 | icsfilename: "NexposeScanSchedule.ics" 27 | icsitterations: 4 28 | staledays: 60 29 | 30 | # Vulnerability Reporter 31 | vulnReporterThreads: 1 32 | ageInterval: 24 33 | vrDebug: "false" 34 | 35 | # Mail Notification Defaults 36 | mailFrom: "Notifier@example.com" 37 | mailServer: "relay@example.com" 38 | mailPort: "25" 39 | mailDomain: "example.com" 40 | defaultEmail: "security@example.com" 41 | -------------------------------------------------------------------------------- /templateVulnList.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 11/12/2014 3 | 4 | require 'yaml' 5 | require 'csv' 6 | require 'nexpose' 7 | require 'pp' 8 | 9 | include Nexpose 10 | 11 | # Default Values 12 | # Default Values from yaml file 13 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 14 | config = YAML.load_file(config_path) 15 | 16 | @host = config["hostname"] 17 | @userid = config["username"] 18 | @password = config["passwordkey"] 19 | @port = config["port"] 20 | 21 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 22 | puts 'logging into Nexpose' 23 | 24 | begin 25 | nsc.login 26 | rescue ::Nexpose::APIError => err 27 | $stderr.puts("Connection failed: #{err.reason}") 28 | exit(1) 29 | end 30 | 31 | puts 'logged into Nexpose' 32 | at_exit { nsc.logout } 33 | 34 | 35 | 36 | begin 37 | 38 | templates = nsc.list_scan_templates 39 | 40 | templates.each do |templateInfo| 41 | puts "TemplateID: #{templateInfo.id}, TemplateName: #{templateInfo.name}" 42 | scanTemplateInfo = Nexpose::ScanTemplate.load(nsc,"#{templateInfo.id}") 43 | 44 | pp scanTemplateInfo 45 | end 46 | end 47 | 48 | exit 49 | -------------------------------------------------------------------------------- /delete_devices_from_group.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # BrianWGray 3 | # 03.19.2018 4 | 5 | # Script Purpose 6 | ## Purge assets listed within a specified group ID. 7 | 8 | ## written as an example for https://kb.help.rapid7.com/discuss/5aaabb3e311eea001e60e862 9 | 10 | require 'yaml' 11 | require 'nexpose' 12 | 13 | include Nexpose 14 | 15 | # Default Values 16 | # Group ID to purge assets from 17 | groupID = 53 18 | 19 | # Default Values from yaml file 20 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 21 | config = YAML.load_file(config_path) 22 | 23 | @host = config["hostname"] 24 | @userid = config["username"] 25 | @password = config["passwordkey"] 26 | @port = config["port"] 27 | 28 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 29 | 30 | begin 31 | nsc.login 32 | rescue ::Nexpose::APIError => err 33 | $stderr.puts("Connection failed: #{err.reason}") 34 | exit(1) 35 | end 36 | 37 | at_exit { nsc.logout if nsc.session_id } 38 | 39 | # load the specified asset group to purge and iterate through devices 40 | assetGroup = Nexpose::AssetGroup.load(nsc, groupID) 41 | 42 | assetGroup.assets.each do |device| 43 | puts "Deleting #{device.address} [Device ID: #{device.id}] Site ID: #{device.site_id} Risk Score: #{device.risk_score}" 44 | nsc.delete_device(device.id) 45 | end 46 | 47 | 48 | -------------------------------------------------------------------------------- /regex_dag.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Date Created: 05.17.2018 4 | # Written by: BrianWGray 5 | 6 | # Written for 7 | # https://kb.help.rapid7.com/v1.0/discuss/5afd940ccbdae50003fe0e2e 8 | 9 | ## Script performs the following tasks 10 | ## 1.) Demonstrate creating a DAG using criterion 11 | ## 2.) Create new asset group 12 | ## 3.) Add addresses to the created asset group based on a regex search. 13 | 14 | require 'yaml' 15 | require 'nexpose' 16 | require 'pp' 17 | include Nexpose 18 | 19 | # Default Values from yaml file 20 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 21 | config = YAML.load_file(config_path) 22 | 23 | host = config["hostname"] 24 | userid = config["username"] 25 | password = config["passwordkey"] 26 | port = config["port"] 27 | 28 | nsc = Nexpose::Connection.new(host, userid, password, port) 29 | 30 | begin 31 | nsc.login 32 | rescue ::Nexpose::APIError => err 33 | $stderr.puts("Connection failed: #{err.reason}") 34 | exit(1) 35 | end 36 | at_exit { nsc.logout } 37 | 38 | assetarray = [] 39 | assetarray << Criterion.new(Search::Field::IP_ADDRESS,Search::Operator::LIKE,"^10\\\.\\\d{1,3}\\\.\\\d{1,3}\\\.11$") 40 | 41 | 42 | crag = Criteria.new(assetarray,"OR") 43 | dag = DynamicAssetGroup.new('test_dag',crag,'test description') 44 | 45 | dag.save(nsc) 46 | 47 | pp(dag) 48 | 49 | exit -------------------------------------------------------------------------------- /nexposeBackup.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 07.14.2014 4 | 5 | ## Script generates a Platform Independent application backup. 6 | 7 | require 'yaml' 8 | require 'nexpose' 9 | 10 | include Nexpose 11 | 12 | # Default Values 13 | 14 | config = YAML.load_file("conf/nexpose.yaml") # From file 15 | 16 | @host = config["hostname"] 17 | @userid = config["username"] 18 | @password = config["passwordkey"] 19 | @port = config["port"] 20 | 21 | 22 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 23 | puts 'logging into Nexpose' 24 | 25 | begin 26 | nsc.login 27 | rescue ::Nexpose::APIError => err 28 | $stderr.puts("Connection failed: #{e.reason}") 29 | exit(1) 30 | end 31 | 32 | puts 'logged into Nexpose' 33 | at_exit { nsc.logout } 34 | 35 | begin 36 | # Check scan activity wait until there are no scans running 37 | active_scans = nsc.scan_activity 38 | if active_scans.any? 39 | puts "Current scan status: #{active_scans.to_s}" 40 | sleep(15) 41 | end 42 | end while active_scans.any? 43 | 44 | time = Time.new 45 | backupDescription = time.strftime("%Y%m%d")+"_PI_Weekly" 46 | 47 | # Base backup code use from https://community.rapid7.com/thread/4687 48 | # Start the backup 49 | if active_scans.empty? 50 | platform_independent = true 51 | puts "Initiating Platform Independent backup to local disk" 52 | nsc.backup(platform_independent, backupDescription) 53 | else 54 | 55 | end 56 | 57 | 58 | puts 'Logging out' 59 | exit 60 | -------------------------------------------------------------------------------- /report_distlist.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Date Created: 05.21.2018 4 | # Written by: BrianWGray 5 | 6 | # Written for 7 | # https://kb.help.rapid7.com/discuss/5b031d8a01b0ff00038d8b9b 8 | 9 | ## Script performs the following tasks 10 | ## 1.) pull report configuration 11 | ## 2.) generate list of report recipients 12 | 13 | require 'yaml' 14 | require 'nexpose' 15 | require 'pp' 16 | include Nexpose 17 | 18 | # Default Values from yaml file 19 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 20 | config = YAML.load_file(config_path) 21 | 22 | host = config["hostname"] 23 | userid = config["username"] 24 | password = config["passwordkey"] 25 | port = config["port"] 26 | 27 | nsc = Nexpose::Connection.new(host, userid, password, port) 28 | 29 | begin 30 | nsc.login 31 | rescue ::Nexpose::APIError => err 32 | $stderr.puts("Connection failed: #{err.reason}") 33 | exit(1) 34 | end 35 | at_exit { nsc.logout } 36 | 37 | # Pull a list of all reports 38 | reportList = nsc.list_reports 39 | 40 | # Iterate through each report 41 | reportList.each do |reportDetails| 42 | # Load report information for each report 43 | reportInfo = ReportConfig.load(nsc, reportDetails.config_id) 44 | 45 | # If the report has external recipients configured, print the recipient list 46 | if(reportInfo.delivery.email.respond_to? :recipients) then 47 | puts("Report: #{reportInfo.name}") 48 | reportInfo.delivery.email.recipients.each do |eachRecipient| 49 | puts(eachRecipient) 50 | end 51 | end 52 | end 53 | 54 | 55 | exit -------------------------------------------------------------------------------- /listEngines.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 06.09.2015 4 | 5 | 6 | # List Engines and data associated with each. 7 | 8 | require 'yaml' 9 | require 'nexpose' 10 | require 'pp' 11 | include Nexpose 12 | 13 | 14 | 15 | #Default Values from yaml file 16 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 17 | config = YAML.load_file(config_path) 18 | 19 | @host = config["hostname"] 20 | @userid = config["username"] 21 | @password = config["passwordkey"] 22 | @port = config["port"] 23 | 24 | 25 | begin 26 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 27 | puts 'logging into Nexpose' 28 | 29 | begin 30 | nsc.login 31 | rescue ::Nexpose::APIError => err 32 | $stderr.puts("Connection failed: #{err.reason}") 33 | exit(1) 34 | end 35 | 36 | puts 'logged into Nexpose' 37 | at_exit { nsc.logout } 38 | 39 | nsc.engines.each do |engine| 40 | engineLoad = Engine.load(nsc,engine.id) 41 | # pp(engineLoad) 42 | puts("Engine: #{engine.name}-#{engine.id} Status: #{engine.status}") 43 | engineLoad.sites.each {|siteData| 44 | siteInfoID = siteData.id 45 | siteDetail = Site.load(nsc, siteInfoID) 46 | # pp(siteDetail) 47 | siteName = siteDetail.name 48 | puts " Site ID: #{siteInfoID} Site Name: #{siteName}" 49 | } 50 | end 51 | 52 | =begin 53 | @nsc.list_engine_pools.each do |engine| 54 | puts(" EnginePool: #{engine.name}-#{engine.id}") 55 | engineLoad = Engine.load(@nsc,engine.id) 56 | pp(engineLoad) 57 | end 58 | =end 59 | 60 | 61 | end 62 | -------------------------------------------------------------------------------- /Nexpose-Lieberman-Integration/nexpose_integration.rb: -------------------------------------------------------------------------------- 1 | class NexposeIntegration 2 | 3 | require 'nexpose' 4 | include Nexpose 5 | 6 | def connect (console, nxuser, nxpass) 7 | @nsc = Connection.new(console, nxuser, nxpass) 8 | @nsc.login 9 | end 10 | 11 | def get_all_sites 12 | all_sites = @nsc.list_sites 13 | end 14 | 15 | def get_hostnames(site) 16 | hostnames = [] 17 | site = Site.load(@nsc, site.id) 18 | assets = site.assets 19 | assets.each do |asset| 20 | if asset.is_a?(HostName) 21 | # Lieberman doesn't like FQDNs 22 | hostname = asset.host.slice(/^[^.]*/) 23 | hostnames.push(hostname) 24 | end 25 | end 26 | end 27 | 28 | def save_credential(asset_info) 29 | site = Site.load(@nsc, asset_info[:site_id]) 30 | newcred = Credential.for_service(asset_info[:service], asset_info[:user], asset_info[:password],asset_info[:realm], asset_info[:hostname], asset_info[:port]) 31 | newcreds = [newcred] 32 | site.credentials = newcreds 33 | site.save(@nsc) 34 | end 35 | 36 | def save_all_credentials_for_site(assets_info, site) 37 | site = Site.load(@nsc, site) 38 | newcreds = [] 39 | assets_info.each do |asset_info| 40 | newcred = Credential.for_service(asset_info[:service], asset_info[:user], asset_info[:password],asset_info[:realm], asset_info[:hostname], asset_info[:port]) 41 | newcreds.push(newcred) 42 | end 43 | site.credentials = newcreds 44 | site.save(@nsc) 45 | end 46 | 47 | def start_scan_site(site) 48 | site = Site.load(@nsc, site) 49 | site.scan(@nsc) 50 | end 51 | end -------------------------------------------------------------------------------- /dhcp_heal.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Date Created: 11.16.2017 4 | # Written by: BrianWGray 5 | 6 | # Written for 7 | # DCHP dynamic connectors are unstable. 8 | # Currently running in an hourly cronjob 9 | 10 | ## Script performs the following tasks 11 | ## 1.) List DHCP dynamic connections 12 | ## 2.) Check connection status 13 | ## 3.) Heal failed connections 14 | ## 4.) TODO: Re-Evaluate connection status for confirmation 15 | 16 | require 'yaml' 17 | require 'nexpose' 18 | require 'pp' 19 | include Nexpose 20 | 21 | # Default Values from yaml file 22 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 23 | config = YAML.load_file(config_path) 24 | 25 | host = config["hostname"] 26 | userid = config["username"] 27 | password = config["passwordkey"] 28 | port = config["port"] 29 | 30 | nsc = Nexpose::Connection.new(host, userid, password, port) 31 | 32 | begin 33 | nsc.login 34 | rescue ::Nexpose::APIError => err 35 | $stderr.puts("Connection failed: #{err.reason}") 36 | exit(1) 37 | end 38 | at_exit { nsc.logout } 39 | 40 | connectionList = nsc.list_discovery_connections 41 | 42 | connectionList.each do | dynCon | 43 | puts("#{dynCon.id} : #{dynCon.name} Status: #{dynCon.status}") 44 | 45 | # Hardcode name of the dhcp connection to check until I determine a cleaner way 46 | if((!dynCon.name.include?("Sonar")) && (dynCon.name.include?("DHCP")) && (dynCon.status.include?("Not Connected"))) 47 | dynCon.type = 'DHCP_SERVICE' 48 | dynCon.collection_method = 'SYSLOG' 49 | dynCon.event_source = 'INFOBLOX_TRINZIC' 50 | dynCon.engine_id = 14 51 | 52 | #pp(dynCon) 53 | puts "Correcting dynamic connection issue on ID #{dynCon.id} : #{dynCon.name}" 54 | end 55 | end 56 | 57 | exit -------------------------------------------------------------------------------- /scanAssetGroup.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Date Created: 04.30.2014 4 | # Written by: BrianWGray 5 | 6 | 7 | ## Script performs the following tasks 8 | ## 1.) Initiates scans for assets located within a specified asset Group ID 9 | 10 | 11 | require 'yaml' 12 | require 'nexpose' 13 | require 'optparse' 14 | require 'highline/import' 15 | require 'csv' 16 | 17 | include Nexpose 18 | 19 | # Default Values 20 | 21 | config = YAML.load_file("conf/nexpose.yaml") # From file 22 | 23 | @host = config["hostname"] 24 | @userid = config["username"] 25 | @password = config["passwordkey"] 26 | @port = config["port"] 27 | 28 | 29 | OptionParser.new do |opts| 30 | opts.banner = "Usage: #{File::basename($0)} [Asset Group ID number] [options]" 31 | opts.separator '' 32 | opts.separator 'This script will re-launch scans against a provided asset group id number.' 33 | opts.separator '' 34 | opts.separator 'Note that this script will always prompt for a connection password.' 35 | opts.separator '' 36 | opts.separator 'Options:' 37 | opts.on_tail('--help', 'Print this help message.') { puts opts; exit } 38 | end.parse! 39 | 40 | unless ARGV[0] 41 | $stderr.puts 'Asset Group ID Required.' 42 | exit(1) 43 | end 44 | 45 | @agid = ARGV[0] 46 | 47 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 48 | puts 'logging into Nexpose' 49 | 50 | begin 51 | nsc.login 52 | rescue ::Nexpose::APIError => err 53 | $stderr.puts("Connection failed: #{err.reason}") 54 | exit(1) 55 | end 56 | 57 | puts 'logged into Nexpose' 58 | at_exit { nsc.logout } 59 | 60 | 61 | puts "Initializing scans for Site ID #{@agid}" 62 | group = AssetGroup.load(nsc, @agid) 63 | scans = group.rescan_assets(nsc) 64 | 65 | puts 'Scan jobs submitted' 66 | puts 'Logging out' 67 | exit 68 | -------------------------------------------------------------------------------- /scan_asset_group.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Date Created: 04.30.2014 4 | # Written by: BrianWGray 5 | 6 | require 'nexpose' 7 | require 'optparse' 8 | require 'highline/import' 9 | 10 | include Nexpose 11 | 12 | # Default Values 13 | 14 | @host = 'localhost' 15 | @port = '3780' 16 | @user = 'nxadmin' 17 | 18 | OptionParser.new do |opts| 19 | opts.banner = "Usage: #{File::basename($0)} [Asset Group ID number] [options]" 20 | opts.separator '' 21 | opts.separator 'This script will re-launch scans against a provided asset group id number.' 22 | opts.separator '' 23 | opts.separator 'Note that this script will always prompt for a connection password.' 24 | opts.separator '' 25 | opts.separator 'Options:' 26 | opts.on('-h', '--host [HOST]', 'IP or hostname of Nexpose console. Default: localhost') { |host| @host = host } 27 | opts.on('-p', '--port [PORT]', Integer, 'Port of Nexpose console. Default: 3780') { |port| @port = port } 28 | opts.on('-u', '--user [USER]', 'Username to connect to Nexpose with. Default: nxadmin') { |user| @user = user } 29 | opts.on_tail('--help', 'Print this help message.') { puts opts; exit } 30 | end.parse! 31 | 32 | unless ARGV[0] 33 | $stderr.puts 'Asset Group ID Required.' 34 | exit(1) 35 | end 36 | 37 | @agid = ARGV[0] 38 | 39 | def get_password(prompt = 'Password: ') 40 | ask(prompt) { |query| query.echo = false } 41 | end 42 | 43 | puts "logging into #{@host} as #{@user} on port #{@port}" 44 | @password = get_password 45 | 46 | 47 | nsc = Nexpose::Connection.new(@host, @user, @password, @port) 48 | puts 'Nexpose login initiated' 49 | nsc.login 50 | 51 | puts 'Nexpose login successful' 52 | 53 | puts "Initializing scans for Asset Group ID #{@agid}" 54 | group = AssetGroup.load(nsc, @agid) 55 | scans = group.rescan_assets(nsc) 56 | 57 | puts 'Scan jobs submitted' 58 | 59 | at_exit { nsc.logout } 60 | puts 'Logging out' 61 | exit 62 | -------------------------------------------------------------------------------- /export_running_log.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'yaml' 4 | require 'nexpose' 5 | include Nexpose 6 | 7 | # Default Values from yaml file 8 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 9 | config = YAML.load_file(config_path) 10 | 11 | @host = config["hostname"] 12 | @userid = config["username"] 13 | @password = config["passwordkey"] 14 | @port = config["port"] 15 | 16 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 17 | 18 | begin 19 | nsc.login 20 | rescue ::Nexpose::APIError => err 21 | $stderr.puts("Connection failed: #{err.reason}") 22 | exit(1) 23 | raise 24 | end 25 | at_exit { nsc.logout } 26 | 27 | # Allow the user to pass in the Scan ID to the script. 28 | scan_id = ARGV[0].to_i 29 | 30 | 31 | 32 | # Export the data associated with a single scan, and optionally store it in 33 | # a zip-compressed file under the provided name. 34 | # 35 | # @param [Fixnum] scan_id Scan ID to remove data for. 36 | # @param [String] zip_file Filename to export scan data to. 37 | # @return [Fixnum] On success, returned the number of bytes written to 38 | # zip_file, if provided. Otherwise, returns raw ZIP binary data. 39 | # 40 | def nsc.scan_log(scan_id, zip_file = nil) 41 | http = AJAX.https(self) 42 | headers = { 'Cookie' => "nexposeCCSessionID=#{@session_id}", 43 | 'Accept-Encoding' => 'identity' } 44 | resp = http.get("/data/scan/log?scan-id=#{scan_id}", headers) 45 | 46 | case resp 47 | when Net::HTTPSuccess 48 | if zip_file 49 | ::File.open(zip_file, 'wb') { |file| file.write(resp.body) } 50 | else 51 | resp.body 52 | end 53 | when Net::HTTPForbidden 54 | raise Nexpose::PermissionError.new(resp) 55 | else 56 | raise Nexpose::APIError.new(resp, "#{resp.class}: Unrecognized response.") 57 | end 58 | end 59 | 60 | 61 | 62 | nsc.scan_log(scan_id, "scan-#{scan_id}.zip") 63 | -------------------------------------------------------------------------------- /stopScansForEngine.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 01.26.2015 4 | 5 | ## Script performs the following tasks 6 | ## 1.) Retrieve a list of active scans from a console. 7 | ## 2.) Iteratively stop all scans for a specific scan engine id. 8 | ## 3.) TODO: Massive code cleanup + efficiency improvements. 9 | 10 | require 'yaml' 11 | require 'nexpose' 12 | 13 | include Nexpose 14 | 15 | 16 | engineID = 6 # engine id to stop 17 | 18 | # Default Values from yaml file 19 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 20 | config = YAML.load_file(config_path) 21 | 22 | @host = config["hostname"] 23 | @userid = config["username"] 24 | @password = config["passwordkey"] 25 | @port = config["port"] 26 | @nexposeAjaxTimeout = config["nexposeajaxtimeout"] 27 | 28 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 29 | begin 30 | nsc.login 31 | rescue ::Nexpose::APIError => err 32 | $stderr.puts("Connection failed: #{err.reason}") 33 | exit(1) 34 | end 35 | at_exit { nsc.logout } 36 | 37 | ## Pull data for active scans 38 | activeScans = nsc.scan_activity() 39 | # Collect Site info to provide additional information for screen output. 40 | siteInfo = nsc.sites 41 | 42 | ## Iterate through active scans and stop scans matching the specified engine id. 43 | activeScans.each do |status| 44 | siteInfoID = status.site_id 45 | begin 46 | if status.engine_id == engineID # This should probably just be an include? engine_id = engineID for the array. 47 | puts "Stopping scanid: #{status.scan_id} on EngineID: #{status.engine_id} for SiteID #{status.site_id} : #{siteInfo[siteInfoID].name}" 48 | nsc.stop_scan(status.scan_id) 49 | end 50 | rescue 51 | puts "Error stopping scanid #{status.scan_id} on EngineID: #{status.engine_id} for SiteID #{status.site_id} : #{siteInfo[siteInfoID].name} to the stop queue" 52 | end 53 | end 54 | 55 | exit -------------------------------------------------------------------------------- /moveEngineToPool.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 04.09.2015 4 | 5 | ## Script performs the following tasks 6 | ## 1.) Stop all scans assigned sites assigned to a specified scan engine. 7 | ## 2.) Stop all scans running on the engine to be removed. 8 | ## 3.) Assign the listed sites from one scan engine to a new scan engine. 9 | ## 4.) TODO: efficiency improvements. 10 | 11 | ## This script was primarily meant to be used for moving sites from engines to scan pools. 12 | 13 | 14 | require 'yaml' 15 | require 'nexpose' 16 | 17 | include Nexpose 18 | 19 | 20 | engineID = 3 # engine id to move from 21 | newEngineID = 6 # engine id to move to 22 | 23 | # Default Values from yaml file 24 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 25 | config = YAML.load_file(config_path) 26 | 27 | @host = config["hostname"] 28 | @userid = config["username"] 29 | @password = config["passwordkey"] 30 | @port = config["port"] 31 | @nexposeAjaxTimeout = config["nexposeajaxtimeout"] 32 | 33 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 34 | begin 35 | nsc.login 36 | rescue ::Nexpose::APIError => err 37 | $stderr.puts("Connection failed: #{err.reason}") 38 | exit(1) 39 | end 40 | at_exit { nsc.logout } 41 | 42 | engineInfo = Nexpose::Engine.load(nsc, engineID) 43 | 44 | engineInfo.sites.each do |engineSites| 45 | begin 46 | puts "Moving Site ID: #{engineSites.id}, Site Name: #{engineSites.name} from EngineID: #{engineID} to EngineID #{newEngineID}" 47 | 48 | begin 49 | siteModify = Nexpose::Site.load(nsc,engineSites.id) 50 | siteModify.engine_id = newEngineID 51 | siteModify.save(nsc) 52 | 53 | rescue ::Nexpose::APIError => err 54 | puts "Error during site modify function: #{err.reason}" 55 | end 56 | 57 | rescue ::Nexpose::APIError => err 58 | puts "Error modifying Site ID: #{engineSites.id}, Site Name: #{engineSites.name}'s scan engine: #{err.reason}" 59 | end 60 | end 61 | 62 | exit 63 | -------------------------------------------------------------------------------- /openPortQuery.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 08/08/2014 3 | 4 | 5 | # Queries heavily draw from: 6 | # https://community.rapid7.com/message/11358#11358 7 | # https://community.rapid7.com/docs/DOC-2612 8 | # 9 | 10 | 11 | 12 | require 'yaml' 13 | require 'nexpose' 14 | require 'csv' 15 | 16 | # Default Values 17 | 18 | config = YAML.load_file("conf/nexpose.yaml") # From file 19 | 20 | @host = config["hostname"] 21 | @userid = config["username"] 22 | @password = config["passwordkey"] 23 | @port = config["port"] 24 | 25 | 26 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 27 | puts 'logging into Nexpose' 28 | 29 | begin 30 | nsc.login 31 | rescue ::Nexpose::APIError => err 32 | $stderr.puts("Connection failed: #{e.reason}") 33 | exit(1) 34 | end 35 | 36 | puts 'logged into Nexpose' 37 | at_exit { nsc.logout } 38 | 39 | 40 | puts "Example: Enter \"23\" if you want to query for port 23." 41 | prompt = 'Please enter a port number to query for: ' 42 | print prompt 43 | #Limiting character count to 32 44 | UserInput = STDIN.gets(32).chomp() 45 | 46 | 47 | 48 | sqlSelect = "SELECT da.ip_address, das.port, dp.name AS protocol, ds.name AS service, dsf.version AS service_version, dsf.name AS service_name, da.host_name, dos.name AS OS, dos.version AS os_version 49 | FROM dim_asset_service das 50 | JOIN dim_service ds USING (service_id) 51 | JOIN dim_protocol dp USING (protocol_id) 52 | JOIN dim_asset da USING (asset_id) 53 | JOIN dim_operating_system dos USING (operating_system_id) 54 | JOIN dim_service_fingerprint dsf USING (service_fingerprint_id) " 55 | 56 | sqlWhere = "Where das.port = #{UserInput}" 57 | 58 | sqlOrderBy = " ORDER BY da.ip_address, das.port;" 59 | 60 | query = sqlSelect + sqlWhere + sqlOrderBy 61 | 62 | 63 | report = Nexpose::AdhocReportConfig.new(nil, 'sql') 64 | report.add_filter('version', '1.2.1') 65 | report.add_filter('query', query) 66 | report_output = report.generate(nsc,18000) # Timeout for report generation is currently set at ~30 minutes 67 | csv_output = CSV.parse(report_output.chomp, { :headers => :first_row }) 68 | CSV.open("openPort_#{UserInput}_export.csv", 'w') do |csv_file| 69 | csv_file << csv_output.headers 70 | csv_output.each do |row| 71 | csv_file << row 72 | end 73 | end 74 | 75 | puts "CSV export completed and saved to ./OpenPort_#{UserInput}_export.csv." 76 | 77 | exit 78 | -------------------------------------------------------------------------------- /add_global_exclusion.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Date Created: 10.18.2018 4 | # Written by: BrianWGray 5 | 6 | # Generated for https://kb.help.rapid7.com/discuss/5bbf13faeb416300039a1efa 7 | 8 | ## Script performs the following tasks 9 | ## 1.) Read addresses from a text file 10 | ## 2.) Add addresses to the Global Exclusion list. 11 | 12 | # TODO: Possibly change the script to allow loading contents from a csv instead of a text file with an asset entry per line. 13 | 14 | ## Currently the script loads a text file with a single entry per line. 15 | # Each asset entry may be: 16 | ## single ip 17 | ## ip range 192.168.0.0/24 | 192.168.0.0-192.168.0.255 18 | ## hostname 19 | 20 | require 'yaml' 21 | require 'nexpose' 22 | include Nexpose 23 | 24 | # Default Values from yaml file 25 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 26 | config = YAML.load_file(config_path) 27 | 28 | @host = config["hostname"] 29 | @userid = config["username"] 30 | @password = config["passwordkey"] 31 | @port = config["port"] 32 | @debug = false 33 | 34 | # Any arguments after flags can be grabbed now." 35 | unless ARGV[0] 36 | $stderr.puts 'Input file is required.' 37 | exit(1) 38 | end 39 | file = ARGV[0] 40 | 41 | def load_file(file) 42 | # This will fail if the file cannot be read. 43 | begin 44 | fileContents = File.read(file).split.uniq 45 | rescue 46 | $stderr.puts "Error reading file: #{file}" 47 | exit(1) 48 | end 49 | 50 | return fileContents 51 | end 52 | 53 | def add_global_exclusion(nsc,assetList) 54 | globalSettings = GlobalSettings.load(nsc) 55 | assetList.each do |asset| 56 | puts "Adding #{asset} to global exclusion list" 57 | globalSettings.add_exclusion(asset) 58 | end 59 | 60 | begin 61 | # Save global exclusion changes 62 | globalSettings.save(nsc) 63 | return true # success 64 | rescue ::Nexpose::APIError => err 65 | $stderr.puts("Saving Global Settings Failed") 66 | $stderr.puts("#{err.reason}") 67 | return false # save failed 68 | end 69 | end 70 | 71 | # Create Nexpose connection and authenticate 72 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 73 | begin 74 | nsc.login 75 | at_exit { nsc.logout } 76 | rescue ::Nexpose::APIError => err 77 | $stderr.puts("Connection failed: #{err.reason}") 78 | exit(1) 79 | end 80 | 81 | # Load asset list from file 82 | assetList = load_file(file) 83 | # Load asset list into global exclusions 84 | add_global_exclusion(nsc,assetList) 85 | 86 | exit() -------------------------------------------------------------------------------- /discoveryCount.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 12.01.2014 4 | 5 | # Put together to try and determine a good way to answer question https://community.rapid7.com/thread/5394 6 | 7 | # Script performs the following tasks 8 | ## 1.) Retrieve a list of available sites from a console. 9 | ## 2.) Retrieve address entries for each site. 10 | ## 3.) Convert address ranges to ip address counts 11 | ## 4.) Provide a total of addresses per site. 12 | ## 5.) Provide a total count of addresses for all sites combined. 13 | 14 | 15 | 16 | require 'yaml' 17 | require 'nexpose' 18 | require 'ipaddr' 19 | include Nexpose 20 | 21 | # Default Values 22 | 23 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 24 | config = YAML.load_file(config_path) 25 | 26 | @host = config["hostname"] 27 | @userid = config["username"] 28 | @password = config["passwordkey"] 29 | @port = config["port"] 30 | 31 | assetCounter = 0 32 | 33 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 34 | puts 'logging into Nexpose' 35 | 36 | begin 37 | nsc.login 38 | rescue ::Nexpose::APIError => err 39 | $stderr.puts("Connection failed: #{err.reason}") 40 | exit(1) 41 | end 42 | 43 | at_exit { nsc.logout } 44 | 45 | site = nsc.list_sites 46 | case site.length 47 | when 0 48 | puts("There are currently no active sites on this NeXpose instance") 49 | end 50 | 51 | 52 | def convert_ip_range(start_ip, end_ip) 53 | start_ip = IPAddr.new(start_ip) 54 | end_ip = IPAddr.new(end_ip) 55 | 56 | (start_ip..end_ip).map(&:to_s) 57 | end 58 | 59 | begin 60 | site.each do |site| 61 | site = Nexpose::Site.load(nsc, site.id) 62 | puts "Getting defined assets for #{site.name}" 63 | site.included_addresses.each do |asset| 64 | if asset.respond_to? :from 65 | 66 | if asset.to != nil 67 | startRange = "#{asset.from}" if asset.to 68 | endRange = "#{asset.to}" 69 | currentCount = convert_ip_range(startRange.to_s, endRange.to_s).count 70 | else 71 | currentCount = 1 72 | end 73 | 74 | assetCounter += currentCount 75 | 76 | puts ("Current Site Address Count: #{currentCount} Total Address Tally: #{assetCounter}") 77 | 78 | end 79 | end 80 | end 81 | end 82 | 83 | puts "Total tally of discoverable addresses for all sites: #{assetCounter}" 84 | 85 | puts 'Logging out' 86 | exit 87 | -------------------------------------------------------------------------------- /search_ip.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 08.22.2016 4 | 5 | # Example on querying whether an ip exists in a specified site. 6 | 7 | # Script performs the following tasks 8 | ## 1.) Retrieve address entries for each site. 9 | ## 2.) check the asset array for a specified address. 10 | ## 3.) Print true or false whether the ip provided is within the site provided 11 | 12 | require 'yaml' 13 | require 'nexpose' 14 | require 'ipaddr' 15 | include Nexpose 16 | 17 | # Default Values 18 | 19 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 20 | config = YAML.load_file(config_path) 21 | 22 | @host = config["hostname"] 23 | @userid = config["username"] 24 | @password = config["passwordkey"] 25 | @port = config["port"] 26 | 27 | siteId = 0 28 | 29 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 30 | 31 | begin 32 | nsc.login 33 | rescue ::Nexpose::APIError => err 34 | $stderr.puts("Connection failed: #{err.reason}") 35 | exit(1) 36 | end 37 | 38 | at_exit { nsc.logout } 39 | 40 | 41 | def convert_ip_range(start_ip, end_ip) 42 | start_ip = IPAddr.new(start_ip) 43 | end_ip = IPAddr.new(end_ip) 44 | 45 | (start_ip..end_ip).map(&:to_s) 46 | end 47 | 48 | def search_ip(assetList, ip) 49 | @assetList, @ip = assetList, ip 50 | @assetList.include?(@ip) 51 | end 52 | 53 | def assets (siteAddresses) 54 | @siteAddresses = siteAddresses 55 | @assetList = [] # => list of ipaddresses in a site 56 | 57 | @siteAddresses.each do |asset| 58 | if asset.respond_to? :from 59 | if asset.to != nil 60 | startRange = "#{asset.from}" if asset.to 61 | endRange = "#{asset.to}" 62 | @assetList << convert_ip_range(startRange.to_s, endRange.to_s) 63 | else 64 | @assetList << asset 65 | end 66 | end 67 | end 68 | return @assetList.flatten 69 | end 70 | 71 | 72 | # Accept arguments for the numerical site ID and an address to search for. 73 | if ARGV.length < 2 74 | # If no argument is passed print usage and exit 75 | puts "usage: #{__FILE__} siteId# Address" 76 | exit 77 | else 78 | siteId, findIp = ARGV[0], ARGV[1] 79 | end 80 | 81 | #TODO: Add validation for whether a site exists prior to attempting to access it 82 | site = Nexpose::Site.load(nsc, siteId) # => Load Nexpose Site Data 83 | assetList = assets(site.included_addresses) # => detonate all site assets into an array 84 | 85 | # provide a detonated ip list and search the array for a specified ip address value 86 | # if the address is within the array true is returned else false returned. 87 | if search_ip(assetList, findIp) == true; puts "true"; else puts "false"; end 88 | 89 | 90 | exit 91 | -------------------------------------------------------------------------------- /powershell/alter_credential.ps1: -------------------------------------------------------------------------------- 1 | # Makes PS ignore self signed certs used by internal servers 2 | # Have someone actually sign this certificate... - BrianWGray 3 | if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type) 4 | { 5 | $certCallback = @" 6 | using System; 7 | using System.Net; 8 | using System.Net.Security; 9 | using System.Security.Cryptography.X509Certificates; 10 | public class ServerCertificateValidationCallback 11 | { 12 | public static void Ignore() 13 | { 14 | if(ServicePointManager.ServerCertificateValidationCallback ==null) 15 | { 16 | ServicePointManager.ServerCertificateValidationCallback += 17 | delegate 18 | ( 19 | Object obj, 20 | X509Certificate certificate, 21 | X509Chain chain, 22 | SslPolicyErrors errors 23 | ) 24 | { 25 | return true; 26 | }; 27 | } 28 | } 29 | } 30 | "@ 31 | Add-Type $certCallback 32 | } 33 | [ServerCertificateValidationCallback]::Ignore() 34 | # https://help.rapid7.com/insightvm/en-us/api/index.html 35 | 36 | # Collects user credentials for login (Functional) 37 | $creds = Get-Credential 38 | $unsecureCreds = $creds.GetNetworkCredential() 39 | $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $unsecureCreds.UserName,$unsecureCreds.Password))) 40 | Remove-Variable unsecureCreds 41 | 42 | # Define data transfer type 43 | $data_type = "application/json" 44 | 45 | # URL Definintions 46 | $hostName = "127.0.0.1" 47 | $port = "3780" 48 | $credentialId = 2 49 | $url = "https://${hostName}:${port}/api/3/shared_credentials/${credentialId}/" 50 | 51 | # Build headers to send to the API. In this case we set the Basic authentication value 52 | $table_headers = @{ 53 | Authorization=("Basic {0}" -f $base64AuthInfo) 54 | } 55 | 56 | # Pull existing object for modification 57 | $credential = Invoke-RestMethod -Method 'GET' -Uri $url -Headers $table_headers 58 | 59 | # Modify credential object 60 | $credential.PSObject.Properties.Remove('links') # The links can be left intact but this is an example of removing an unnecessary element 61 | $credential.description = 'This is a modified description' 62 | $credential.account | Add-Member NoteProperty password('altered password') 63 | 64 | # Build PUT Content using the existing credential object 65 | $json = $credential | Convertto-JSON 66 | 67 | # Visual of the json being sent - just for show 68 | $json 69 | 70 | $data = Invoke-RestMethod -Method 'PUT' -Uri $url -ContentType $data_type -Headers $table_headers -Body $json; 71 | -------------------------------------------------------------------------------- /powershell/example_site_get.ps1: -------------------------------------------------------------------------------- 1 | # Makes PS ignore self signed certs used by internal servers 2 | # Have someone actually sign this certificate... - BrianWGray 3 | if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type) 4 | { 5 | $certCallback = @" 6 | using System; 7 | using System.Net; 8 | using System.Net.Security; 9 | using System.Security.Cryptography.X509Certificates; 10 | public class ServerCertificateValidationCallback 11 | { 12 | public static void Ignore() 13 | { 14 | if(ServicePointManager.ServerCertificateValidationCallback ==null) 15 | { 16 | ServicePointManager.ServerCertificateValidationCallback += 17 | delegate 18 | ( 19 | Object obj, 20 | X509Certificate certificate, 21 | X509Chain chain, 22 | SslPolicyErrors errors 23 | ) 24 | { 25 | return true; 26 | }; 27 | } 28 | } 29 | } 30 | "@ 31 | Add-Type $certCallback 32 | } 33 | [ServerCertificateValidationCallback]::Ignore() 34 | 35 | # Check out the following links to get yourself started: - BrianWGray 36 | # https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-restmethod?view=powershell-6 37 | # https://www.gngrninja.com/script-ninja/2016/7/24/powershell-getting-started-utilizing-the-web-part-2-invoke-restmethod 38 | # https://127.0.0.1:3780/api/3/ 39 | # https://help.rapid7.com/insightvm/en-us/api/index.html 40 | 41 | # Collects user credentials for login (Functional) 42 | # I don't highly recommend this specific credential collection method it's just to get the script bootstrapped until you are more comfortable - BrianWGray 43 | $creds = Get-Credential 44 | $unsecureCreds = $creds.GetNetworkCredential() 45 | $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $unsecureCreds.UserName,$unsecureCreds.Password))) 46 | Remove-Variable unsecureCreds 47 | 48 | # Set the API URI to call : In this example a list of sites (visit this in your authenticated browser for what you should be requesting) - BrianWGray 49 | # $url = "https://127.0.0.1:3780/api/3/sites" 50 | $url = "https://127.0.0.1/api/3/shared_credentials/" 51 | 52 | # Interact with the API in this case send a GET request to the specified URL and supply a basic auth header with a base64 encoded username:password 53 | $data = Invoke-RestMethod -Method 'Get' -Uri $url -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} 54 | 55 | # Now we have a data object full of the API response content that can be manipulated for view however you would like. - BrianWGray 56 | $data | Get-Member 57 | 58 | -------------------------------------------------------------------------------- /nexposeListBackups.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 09.08.2014 4 | 5 | ## Script lists available application backup. 6 | 7 | require 'yaml' 8 | require 'nexpose' 9 | 10 | include Nexpose 11 | 12 | # Default Values 13 | 14 | # Default Values from yaml file 15 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 16 | config = YAML.load_file(config_path) 17 | 18 | @host = config["hostname"] 19 | @userid = config["username"] 20 | @password = config["passwordkey"] 21 | @port = config["port"] 22 | @serviceTimeout = config["servicetimeout"] 23 | 24 | 25 | def checkService() 26 | tryAgain = 0 27 | 28 | begin 29 | begin 30 | path = '/login.html' # Check to see if we may login or if we are re-directed to the maintenance login page. 31 | 32 | http = Net::HTTP.new(@host,@port) 33 | http.read_timeout = 1 34 | http.use_ssl = true 35 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 36 | response = nil 37 | 38 | http.start{|http| 39 | request = Net::HTTP::Get.new(path) 40 | response = http.request(request) 41 | } 42 | 43 | rescue Exception # should really list all the possible http exceptions 44 | puts "Attempt: #{tryAgain} Service Unavailable" 45 | sleep (30) 46 | retry if (tryAgain += 1) < @serviceTimeout 47 | end 48 | 49 | response.code 50 | if response.code == "200" # Check the status code anything other than 200 indicates the service is not ready. 51 | puts "Attempt: #{tryAgain} #{response.code} The Nexpose Service appears to be up and functional" 52 | tryAgain = @serviceTimeout 53 | else 54 | puts "Attempt: #{tryAgain} #{response.code} #{response.message} The Service is not yet fully initialized" 55 | tryAgain += 1 56 | sleep(30) 57 | end 58 | end while tryAgain < @serviceTimeout 59 | 60 | if (response.code != "200") 61 | puts "The service was never determined to be available. Action Timed Out" 62 | exit 63 | end 64 | end 65 | 66 | 67 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 68 | 69 | begin 70 | nsc.login 71 | rescue ::Nexpose::APIError => err 72 | $stderr.puts("Connection failed: #{err.reason}") 73 | exit(1) 74 | end 75 | 76 | at_exit { nsc.logout } 77 | 78 | 79 | # Check scan activity wait until there are no scans running 80 | listBackups = nsc.list_backups 81 | if listBackups.any? 82 | puts "List of available Backups on #{@host} :\r\n" 83 | listBackups.each do |backupList| 84 | puts "Name: #{backupList.name} Description: #{backupList.description} size: #{backupList.size} Date : #{backupList.date}" 85 | end 86 | 87 | end 88 | 89 | exit 90 | -------------------------------------------------------------------------------- /create_asset_group.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'nexpose' 3 | require 'optparse' 4 | require 'highline/import' 5 | 6 | # Default values 7 | 8 | @host = "localhost" 9 | @port = "3780" 10 | @user = "nxadmin" 11 | @password = "" 12 | 13 | @name = @desc = nil 14 | 15 | OptionParser.new do |opts| 16 | opts.banner = "Usage: #{File::basename($0)} [options]" 17 | opts.separator '' 18 | opts.separator 'Create an asset group based upon an input file, one IP per line.' 19 | opts.separator '' 20 | opts.separator 'By default, it uses the name of the file as the name of the asset group.' 21 | opts.separator 'As currently written, the script will only add one asset per IP address.' 22 | opts.separator 'If multiple sites have the same IP, it is non-deterministic which asset it will choose.' 23 | opts.separator '' 24 | opts.separator 'Note that this script will always prompt for a connection password.' 25 | opts.separator '' 26 | opts.separator 'Options:' 27 | opts.on('-n', '--name [NAME]', 'Name to use for new asset group. Must not already exist.') { |name| @name = name } 28 | opts.on('-d', '--desc [DESCRIPTION]', 'Description to use for new asset group.') { |desc| @desc = desc } 29 | opts.on('-h', '--host [HOST]', 'IP or hostname of Nexpose console. Default: localhost') { |host| @host = host } 30 | opts.on('-p', '--port [PORT]', Integer, 'Port of Nexpose console. Default: 3780') { |port| @port = port } 31 | opts.on('-u', '--user [USER]', 'Username to connect to Nexpose with. Default: nxadmin') { |user| @user = user } 32 | opts.on('-x', '--debug', 'Report duplicate IP addresses to STDERR.') { |debug| @debug = debug } 33 | opts.on_tail('--help', 'Print this help message.') { puts opts; exit } 34 | end.parse! 35 | 36 | # Any arguments after flags can be grabbed now." 37 | unless ARGV[0] 38 | $stderr.puts 'Input file is required.' 39 | exit(1) 40 | end 41 | file = ARGV[0] 42 | @name = File.basename(file, File.extname(file)) unless @name 43 | 44 | def get_password(prompt = 'Password: ') 45 | ask(prompt) { |query| query.echo = false } 46 | end 47 | 48 | @password = get_password 49 | 50 | # This will fail if the file cannot be read. 51 | ips = File.read(file).split.uniq 52 | 53 | nsc = Nexpose::Connection.new(@host, @user, @password, @port) 54 | nsc.login 55 | 56 | # Create a map of all assets by IP to make them quicker to find. 57 | all_assets = nsc.assets.reduce({}) do |hash, dev| 58 | $stderr.puts("Duplicate asset: #{dev.address}") if @debug and hash.member? dev.address 59 | hash[dev.address] = dev 60 | hash 61 | end 62 | 63 | # Drop the connection, in case group creation takes too long. 64 | nsc.logout 65 | 66 | group = Nexpose::AssetGroup.new(@name, @desc) 67 | 68 | ips.each do |ip| 69 | if all_assets.member? ip 70 | group.devices << all_assets[ip] 71 | elsif @debug 72 | $stderr.puts("No asset with IP #{ip} found.") 73 | end 74 | end 75 | 76 | nsc.login 77 | at_exit { nsc.logout } 78 | group.save(nsc) 79 | puts "Group '#{@name}' saved with #{group.devices.size} assets." 80 | -------------------------------------------------------------------------------- /addToAssetGroup.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Date Created: 07.21.2017 4 | # Written by: BrianWGray 5 | 6 | # Borrows heavily from https://github.com/rapid7/nexpose-client/blob/master/scripts/create_asset_group.rb 7 | # Generated for https://community.rapid7.com/thread/7584 8 | 9 | ## Script performs the following tasks 10 | ## 1.) Read addresses from text file 11 | ## 2.) De-duplicate addresses 12 | ## 3.) Add addresses to the specified asset group id. 13 | 14 | require 'yaml' 15 | require 'nexpose' 16 | require 'optparse' 17 | 18 | include Nexpose 19 | 20 | # Default Values from yaml file 21 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 22 | config = YAML.load_file(config_path) 23 | 24 | @host = config["hostname"] 25 | @userid = config["username"] 26 | @password = config["passwordkey"] 27 | @port = config["port"] 28 | 29 | @groupid = nil 30 | 31 | OptionParser.new do |opts| 32 | opts.banner = "Usage: #{File::basename($0)} addresses.txt [options]" 33 | opts.separator '' 34 | opts.separator 'Add assets to an existing asset group based upon an input file, one IP per line.' 35 | opts.separator '' 36 | opts.separator 'A group id must be provided.' 37 | opts.separator 'If multiple sites include the same address, it is non-deterministic which asset it will choose.' 38 | opts.separator '' 39 | opts.separator 'Options:' 40 | opts.on('-i', '--groupid [groupid]', 'Group ID you are adding to. Must already exist.') { |groupid| @groupid = groupid } 41 | opts.on('-x', '--debug', 'Report duplicate IP addresses to STDERR.') { |debug| @debug = debug } 42 | opts.on_tail('--help', 'Print this help message.') { puts opts; exit } 43 | end.parse! 44 | 45 | # Any arguments after flags can be grabbed now." 46 | unless ARGV[0] 47 | $stderr.puts 'Input file is required.' 48 | exit(1) 49 | end 50 | file = ARGV[0] 51 | 52 | # This will fail if the file cannot be read. 53 | ips = File.read(file).split.uniq 54 | puts "#{file} loaded." 55 | 56 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 57 | puts 'logging into Nexpose' 58 | 59 | begin 60 | nsc.login 61 | rescue ::Nexpose::APIError => err 62 | $stderr.puts("Connection failed: #{err.reason}") 63 | exit(1) 64 | end 65 | 66 | puts 'logged into Nexpose' 67 | at_exit { nsc.logout } 68 | 69 | # Create a map of all assets by IP to make them quicker to find. 70 | all_assets = nsc.assets.reduce({}) do |hash, dev| 71 | $stderr.puts("Duplicate asset: #{dev.address}") if @debug and hash.member? dev.address 72 | hash[dev.address] = dev 73 | hash 74 | end 75 | 76 | # Drop the connection, in case group creation takes too long. 77 | # nsc.logout 78 | 79 | group = Nexpose::AssetGroup.load(nsc, @groupid) 80 | 81 | ips.each do |ip| 82 | puts "Adding #{ip}" 83 | if all_assets.member? ip 84 | group.assets << all_assets[ip] 85 | elsif @debug 86 | $stderr.puts("No asset with IP #{ip} found.") 87 | end 88 | end 89 | 90 | # nsc.login 91 | group.save(nsc) 92 | puts "Group '#{group.id}:#{group.name}' saved with #{group.devices.size} assets." 93 | 94 | exit -------------------------------------------------------------------------------- /createAssetGroup.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Date Created: 04.30.2014 4 | # Written by: BrianWGray 5 | 6 | # Original script sourced from [mdaines-r7] https://github.com/rapid7/nexpose-client/blob/master/scripts/create_asset_group.rb 7 | 8 | ## Script performs the following tasks 9 | ## 1.) Read addresses from text file 10 | ## 2.) De-duplicate addresses 11 | ## 3.) Create new asset group 12 | ## 4.) Add addresses to the created asset group 13 | 14 | require 'yaml' 15 | require 'nexpose' 16 | require 'optparse' 17 | require 'highline/import' 18 | 19 | # Default Values 20 | 21 | config = YAML.load_file("conf/nexpose.yaml") # From file 22 | 23 | @host = config["hostname"] 24 | @userid = config["username"] 25 | @password = config["passwordkey"] 26 | @port = config["port"] 27 | 28 | @name = @desc = nil 29 | 30 | OptionParser.new do |opts| 31 | opts.banner = "Usage: #{File::basename($0)} [options]" 32 | opts.separator '' 33 | opts.separator 'Create an asset group based upon an input file, one IP per line.' 34 | opts.separator '' 35 | opts.separator 'By default, it uses the name of the file as the name of the asset group and does not check if name exists.' 36 | opts.separator 'If multiple sites include the same address, it is non-deterministic which asset it will choose.' 37 | opts.separator '' 38 | opts.separator 'Options:' 39 | opts.on('-n', '--name [NAME]', 'Name to use for new asset group. Must not already exist.') { |name| @name = name } 40 | opts.on('-d', '--desc [DESCRIPTION]', 'Description to use for new asset group.') { |desc| @desc = desc } 41 | opts.on('-x', '--debug', 'Report duplicate IP addresses to STDERR.') { |debug| @debug = debug } 42 | opts.on_tail('--help', 'Print this help message.') { puts opts; exit } 43 | end.parse! 44 | 45 | # Any arguments after flags can be grabbed now." 46 | unless ARGV[0] 47 | $stderr.puts 'Input file is required.' 48 | exit(1) 49 | end 50 | file = ARGV[0] 51 | @name = File.basename(file, File.extname(file)) unless @name 52 | 53 | # This will fail if the file cannot be read. 54 | ips = File.read(file).split.uniq 55 | 56 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 57 | puts 'logging into Nexpose' 58 | 59 | begin 60 | nsc.login 61 | rescue ::Nexpose::APIError => err 62 | $stderr.puts("Connection failed: #{e.reason}") 63 | exit(1) 64 | end 65 | 66 | puts 'logged into Nexpose' 67 | at_exit { nsc.logout } 68 | 69 | # Create a map of all assets by IP to make them quicker to find. 70 | all_assets = nsc.assets.reduce({}) do |hash, dev| 71 | $stderr.puts("Duplicate asset: #{dev.address}") if @debug and hash.member? dev.address 72 | hash[dev.address] = dev 73 | hash 74 | end 75 | 76 | # Drop the connection, in case group creation takes too long. 77 | nsc.logout 78 | 79 | group = Nexpose::AssetGroup.new(@name, @desc) 80 | 81 | ips.each do |ip| 82 | if all_assets.member? ip 83 | group.devices << all_assets[ip] 84 | elsif @debug 85 | $stderr.puts("No asset with IP #{ip} found.") 86 | end 87 | end 88 | 89 | nsc.login 90 | group.save(nsc) 91 | puts "Group '#{@name}' saved with #{group.devices.size} assets." 92 | 93 | exit -------------------------------------------------------------------------------- /deleteStaleAssets.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # WhyIsThisOpen 3 | # 03.02.2015 4 | 5 | # Fixed yaml relative path issues with running the script from outside of its directory. - BrianWGray 07.20.2015 6 | # Fixed error output typo. - BrianWGray 07.20.2015 7 | # Slapped in some basic output formatting. - BrianWGray 07.20.2015 8 | 9 | ## Script deletes stale assets that are part of a site with a scheduled scan. 10 | 11 | require 'yaml' 12 | require 'nexpose' 13 | 14 | include Nexpose 15 | 16 | # Default Values 17 | 18 | # Default Values from yaml file 19 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 20 | config = YAML.load_file(config_path) 21 | 22 | @host = config["hostname"] 23 | @userid = config["username"] 24 | @password = config["passwordkey"] 25 | @port = config["port"] 26 | @staleDays = config["staledays"] 27 | @cleanupWaitTime = config["cleanupwaittime"] 28 | 29 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 30 | puts 'logging into Nexpose' 31 | 32 | begin 33 | nsc.login 34 | rescue ::Nexpose::APIError => err 35 | $stderr.puts("Connection failed: #{err.reason}") 36 | exit(1) 37 | end 38 | 39 | puts 'logged into Nexpose' 40 | at_exit { nsc.logout } 41 | 42 | # Check scan activity and wait until there are no scans running 43 | begin 44 | active_scans = nsc.scan_activity 45 | if active_scans.any? 46 | puts "Active Scans:" 47 | ## Pull data for active scans 48 | activeScans = nsc.scan_activity() 49 | ## Output a list of active scans in the scan queue. 50 | activeScans.each do |status| 51 | siteInfoID = status.site_id 52 | siteDetail = Site.load(nsc, siteInfoID) 53 | begin 54 | Scan 55 | puts "ScanID: #{status.scan_id}, Assets: #{status.nodes.live}, ScanTemplate: #{siteDetail.scan_template_id}, SiteID: #{status.site_id} - #{siteDetail.name}, Status:#{status.status}, EngineID:#{status.engine_id}, StartTime:#{status.start_time}" 56 | rescue 57 | raise 58 | end 59 | 60 | end 61 | puts "Checking for scans again in #{@cleanupWaitTime} seconds." 62 | sleep(@cleanupWaitTime) 63 | end 64 | end while active_scans.any? 65 | 66 | # Determine which sites are being scanned on a schedule 67 | scheduledSites = Array.new 68 | sites = nsc.list_sites 69 | sites.each do |site| 70 | site = Nexpose::Site.load(nsc, site.id) 71 | if site.schedules.any? 72 | scheduledSites << site.id 73 | else 74 | puts "No scheduled scans for SiteID: #{site.id} SiteName: #{site.name}" 75 | end 76 | end 77 | 78 | # Find assets that have not been scanned in the last @staleDays. 79 | old_assets = nsc.filter(Search::Field::SCAN_DATE, Search::Operator::EARLIER_THAN, @staleDays) 80 | 81 | # Iterate through the assets and delete those in sites with schedules. 82 | old_assets.each do |device| 83 | if scheduledSites.include?(device.site_id) 84 | puts "Deleting #{device.ip} [ID: #{device.id}] Site: #{device.site_id} Last Scanned: #{device.last_scan}" 85 | nsc.delete_device(device.id) 86 | end 87 | end 88 | 89 | puts 'Logging out' 90 | exit 91 | 92 | -------------------------------------------------------------------------------- /powershell/alter_credential_build.ps1: -------------------------------------------------------------------------------- 1 | # Makes PS ignore self signed certs used by internal servers 2 | # Have someone actually sign this certificate... - BrianWGray 3 | if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type) 4 | { 5 | $certCallback = @" 6 | using System; 7 | using System.Net; 8 | using System.Net.Security; 9 | using System.Security.Cryptography.X509Certificates; 10 | public class ServerCertificateValidationCallback 11 | { 12 | public static void Ignore() 13 | { 14 | if(ServicePointManager.ServerCertificateValidationCallback ==null) 15 | { 16 | ServicePointManager.ServerCertificateValidationCallback += 17 | delegate 18 | ( 19 | Object obj, 20 | X509Certificate certificate, 21 | X509Chain chain, 22 | SslPolicyErrors errors 23 | ) 24 | { 25 | return true; 26 | }; 27 | } 28 | } 29 | } 30 | "@ 31 | Add-Type $certCallback 32 | } 33 | [ServerCertificateValidationCallback]::Ignore() 34 | # https://help.rapid7.com/insightvm/en-us/api/index.html 35 | 36 | # Collects user credentials for login (Functional) 37 | $creds = Get-Credential 38 | $unsecureCreds = $creds.GetNetworkCredential() 39 | $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $unsecureCreds.UserName,$unsecureCreds.Password))) 40 | Remove-Variable unsecureCreds 41 | 42 | # Define data transfer type 43 | $data_type = "application/json" 44 | 45 | # URL Definintions 46 | $hostName = "127.0.0.1" 47 | $port = "3780" 48 | $credentialId = 2 49 | $url = "https://${hostName}:${port}/api/3/shared_credentials/${credentialId}/" 50 | 51 | # Build headers to send to the API. In this case we set the Basic authentication value 52 | $table_headers = @{ 53 | Authorization=("Basic {0}" -f $base64AuthInfo) 54 | } 55 | 56 | # Pull existing object for modification 57 | $credential = Invoke-RestMethod -Method 'GET' -Uri $url -Headers $table_headers 58 | 59 | $credential.account 60 | 61 | # Credential information use for building a json submission 62 | $credName = $credential.account.username 63 | $service = $credential.account.service 64 | $domain = $credential.account.domain 65 | $userName = $credential.account.username 66 | $userPass = "N3wSVCCr3d3nt1al" 67 | $description = "An altered description" 68 | $siteAssignment = $credential.siteAssignment 69 | 70 | # If you wanted to build the content for a credential object you could build it like: 71 | $body = @{ 72 | account = @{ 73 | domain = $domain; 74 | service = $service; 75 | username = $userName; 76 | password = $userPass; 77 | }; 78 | description = $description; 79 | id = $credentialId; 80 | name = $credName; 81 | siteAssignment = $siteAssignment; 82 | } 83 | 84 | 85 | # Build PUT Content using the existing credential object 86 | $json = $body | Convertto-JSON 87 | 88 | # Visual of the json being sent - just for show 89 | $json 90 | 91 | $data = Invoke-RestMethod -Method 'PUT' -Uri $url -ContentType $data_type -Headers $table_headers -Body $json; 92 | -------------------------------------------------------------------------------- /powershell/create_asset_group.ps1: -------------------------------------------------------------------------------- 1 | # Attempt to help with https://kb.help.rapid7.com/discuss/5c66e04394dea300577d6d47 2 | 3 | # Makes PS ignore self signed certs used by internal servers 4 | # Have someone actually sign this certificate... - BrianWGray 5 | 6 | if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type) 7 | { 8 | $certCallback = @" 9 | using System; 10 | using System.Net; 11 | using System.Net.Security; 12 | using System.Security.Cryptography.X509Certificates; 13 | public class ServerCertificateValidationCallback 14 | { 15 | public static void Ignore() 16 | { 17 | if(ServicePointManager.ServerCertificateValidationCallback ==null) 18 | { 19 | ServicePointManager.ServerCertificateValidationCallback += 20 | delegate 21 | ( 22 | Object obj, 23 | X509Certificate certificate, 24 | X509Chain chain, 25 | SslPolicyErrors errors 26 | ) 27 | { 28 | return true; 29 | }; 30 | } 31 | } 32 | } 33 | "@ 34 | Add-Type $certCallback 35 | } 36 | [ServerCertificateValidationCallback]::Ignore() 37 | # https://help.rapid7.com/insightvm/en-us/api/index.html 38 | # https://help.rapid7.com/insightvm/en-us/api/index.html#operation/getAssetGroups 39 | 40 | # Collects user credentials for login (Functional) 41 | $creds = Get-Credential 42 | $unsecureCreds = $creds.GetNetworkCredential() 43 | $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $unsecureCreds.UserName,$unsecureCreds.Password))) 44 | Remove-Variable unsecureCreds 45 | 46 | # Define data transfer type 47 | $data_type = "application/json" 48 | 49 | # URL Definintions 50 | $hostName = "nexpose-test.example.com" 51 | $port = "3780" 52 | $url = "https://${hostName}:${port}/api/3/asset_groups/" 53 | 54 | # Build headers to send to the API. In this case we set the Basic authentication value 55 | $table_headers = @{ 56 | "Content-Type" = $data_type 57 | Authorization=("Basic {0}" -f $base64AuthInfo) 58 | } 59 | 60 | # Search filter criteria 61 | 62 | $filter = @{ field = "operating-system"; operator = "contains"; value = "linux"} 63 | 64 | $filters = @($filter) 65 | 66 | # Here we're using straight JSON to prove API / documentation issues 67 | $body = @" 68 | { 69 | "description": "A Static Asset Group with Assets that are Linux Assets running Containers (With Low Access Complexity Vulnerabilities) for remediation purposes.", 70 | "name": "Container Hosts - Linux", 71 | "searchCriteria": { 72 | "filters": [ 73 | { "field": "operating-system", "operator": "contains", "value": "linux" }, 74 | { "field": "containers", "operator": "are", "value": 0 }, 75 | { "field": "cvss-access-complexity", "operator": "is", "value": "L" } 76 | ], 77 | "match": "all" 78 | }, 79 | "type": "static" 80 | } 81 | 82 | "@ 83 | 84 | # Build Post Content using the existing credential object 85 | # We don't need to use the convert to JSON in this example but left it for additional testing. 86 | $json = $body # | Convertto-JSON 87 | 88 | # Visual of the json being sent - just for show 89 | $json 90 | 91 | $data = Invoke-RestMethod -Method 'Post' -Uri $url -Headers $table_headers -Body $json -------------------------------------------------------------------------------- /stopPausedScans.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 11.10.2014 4 | 5 | ## Script performs the following tasks 6 | ## 1.) Retrieve a list of paused scans from a console. 7 | ## 2.) Iteratively stop scans that have paused without completing. 8 | ## 3.) TODO: Massive code cleanup + efficiency improvements. 9 | 10 | require 'yaml' 11 | require 'nexpose' 12 | 13 | include Nexpose 14 | 15 | # Default Values from yaml file 16 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 17 | config = YAML.load_file(config_path) 18 | 19 | @host = config["hostname"] 20 | @userid = config["username"] 21 | @password = config["passwordkey"] 22 | @port = config["port"] 23 | @consecutiveCleanupScans = config["cleanupqueue"] 24 | @cleanupWaitTime = config["cleanupwaittime"] 25 | @nexposeAjaxTimeout = config["nexposeajaxtimeout"] 26 | 27 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 28 | puts 'logging into Nexpose' 29 | 30 | begin 31 | nsc.login 32 | rescue ::Nexpose::APIError => err 33 | $stderr.puts("Connection failed: #{e.reason}") 34 | exit(1) 35 | end 36 | 37 | puts 'logged into Nexpose' 38 | at_exit { nsc.logout } 39 | 40 | 41 | ## Initialize connection timeout values. 42 | ## Timeout example provided by JGreen in https://community.rapid7.com/thread/5075 43 | 44 | module Nexpose 45 | class APIRequest 46 | include XMLUtils 47 | # Execute an API request 48 | def self.execute(url, req, api_version='2.0', options = {}) 49 | options = {timeout: @nexposeAjaxTimeout} 50 | obj = self.new(req.to_s, url, api_version) 51 | obj.execute(options) 52 | return obj 53 | end 54 | end 55 | 56 | 57 | module AJAX 58 | def self._https(nsc) 59 | http = Net::HTTP.new(nsc.host, nsc.port) 60 | http.use_ssl = true 61 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 62 | http.read_timeout = @nexposeAjaxTimeout 63 | http 64 | end 65 | end 66 | end 67 | 68 | 69 | ## Start loop that will continue until there are no longer any scans that are paused. 70 | begin 71 | begin 72 | puts "\r\nRequesting scan status updates from #{@host}\r\n" 73 | ## Pull data for paused scans - Method suggested by JGreen https://community.rapid7.com/thread/5075 (THANKS!!!) 74 | pausedScans = DataTable._get_dyn_table(nsc, '/data/site/scans/dyntable.xml?printDocType=0&tableID=siteScansTable&activeOnly=true').select { |scanHistory| (scanHistory['Status'].include? 'Paused')} 75 | rescue Exception # should really list all the possible http exceptions 76 | puts "Connection issue detected - Retrying in 30 seconds" 77 | sleep (120) 78 | retry 79 | end 80 | 81 | ## Loop through paused scans for cleanup. 82 | pausedScans.each do |scanHistory| 83 | 84 | scanIDReport = scanHistory['Scan ID'] 85 | statusReport = scanHistory['Status'] 86 | discoveredReport = scanHistory['Devices Discovered'] 87 | puts "Stopping ScanID: #{scanIDReport}, Discovered: #{discoveredReport} - #{statusReport}" 88 | ## Stop the provided scanid. 89 | nsc.stop_scan(scanIDReport) 90 | end 91 | 92 | 93 | ## If there are no more paused scans, we can exit. 94 | end while ((pausedScans.count) > 0) 95 | 96 | puts "No Paused scans were returned in the request. Exiting" 97 | 98 | puts 'Logging out' 99 | exit -------------------------------------------------------------------------------- /powershell/example_start_scan_post.ps1: -------------------------------------------------------------------------------- 1 | # Makes PS ignore self signed certs used by internal servers 2 | # Have someone actually sign this certificate... - BrianWGray 3 | if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type) 4 | { 5 | $certCallback = @" 6 | using System; 7 | using System.Net; 8 | using System.Net.Security; 9 | using System.Security.Cryptography.X509Certificates; 10 | public class ServerCertificateValidationCallback 11 | { 12 | public static void Ignore() 13 | { 14 | if(ServicePointManager.ServerCertificateValidationCallback ==null) 15 | { 16 | ServicePointManager.ServerCertificateValidationCallback += 17 | delegate 18 | ( 19 | Object obj, 20 | X509Certificate certificate, 21 | X509Chain chain, 22 | SslPolicyErrors errors 23 | ) 24 | { 25 | return true; 26 | }; 27 | } 28 | } 29 | } 30 | "@ 31 | Add-Type $certCallback 32 | } 33 | [ServerCertificateValidationCallback]::Ignore() 34 | 35 | # Check out the following links to get yourself started: - BrianWGray 36 | # https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-restmethod?view=powershell-6 37 | # https://www.gngrninja.com/script-ninja/2016/7/24/powershell-getting-started-utilizing-the-web-part-2-invoke-restmethod 38 | # https://127.0.0.1:3780/api/3/ 39 | # https://help.rapid7.com/insightvm/en-us/api/index.html 40 | 41 | # Collects user credentials for login (Functional) 42 | # I don't highly recommend this specific credential collection method it's just to get the script bootstrapped until you are more comfortable - BrianWGray 43 | $creds = Get-Credential 44 | $unsecureCreds = $creds.GetNetworkCredential() 45 | $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $unsecureCreds.UserName,$unsecureCreds.Password))) 46 | Remove-Variable unsecureCreds 47 | 48 | # Set the API URI to call : In this example we specify the site ID integer that we want to start a scan on - BrianWGray 49 | # $url = "https://127.0.0.1:3780/api/3/sites/1/scans" 50 | 51 | $data_type = "application/json" 52 | 53 | # Build headers to send to the API. In this case we set the data type of the body content and the Basic authentication value 54 | $table_headers = @{ 55 | "Content-Type" = $data_type 56 | Authorization=("Basic {0}" -f $base64AuthInfo) 57 | } 58 | 59 | # Build a POST Body to send to the API 60 | # There are other options but we are just providing a name for the scan for an example 61 | # https://help.rapid7.com/insightvm/en-us/api/index.html#operation/startScan 62 | $body = @{ 63 | name="API Scan Start" 64 | } 65 | $json = $body | Convertto-JSON 66 | 67 | # Interact with the API in this case send a POST request to the specified URL and supply a basic auth header with a base64 encoded username:password 68 | # The $body variable holds the data that we want to provide to the API then we convert the $body content to $json format for the API to parse. 69 | $data = Invoke-RestMethod -Method 'Post' -Uri $url -Headers $table_headers -Body $json 70 | 71 | # Now we have a data object full of the API response content that can be manipulated for view however you would like. - BrianWGray 72 | # $data | Get-Member # Show returned object attributes 73 | $data.id # display the id of the scan that was started 74 | 75 | 76 | -------------------------------------------------------------------------------- /dbMaint.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 07.14.2014 4 | 5 | ## Script runs the following database maintenance tasks 6 | ## 1.) Clean up database - Removes any unnecessary data from the database 7 | ## 2.) Compress database tables - Compresses the database tables and reclaims unused, allocated space. 8 | ## 3.) Re-index database - Drops and recreates the database indexes for improved performance. 9 | 10 | require 'yaml' # Add support for external configurations via yaml file. 11 | require 'net/http' # Used to check whether the nexpose service is available. 12 | require 'nexpose' # Add nexpose-client gem to interact with Nexpose. 13 | 14 | include Nexpose 15 | 16 | # Default Values 17 | 18 | config = YAML.load_file("conf/nexpose.yaml") # From file 19 | 20 | @host = config["hostname"] 21 | @userid = config["username"] 22 | @password = config["passwordkey"] 23 | @port = config["port"] 24 | @serviceTimeout = config["servicetimeout"] 25 | 26 | 27 | def checkService() 28 | tryAgain = 0 29 | 30 | begin 31 | begin 32 | path = '/login.html' # Check to see if we may login or if we are re-directed to the maintenance login page. 33 | 34 | http = Net::HTTP.new(@host,@port) 35 | http.read_timeout = 1 36 | http.use_ssl = true 37 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 38 | response = nil 39 | 40 | http.start{|http| 41 | request = Net::HTTP::Get.new(path) 42 | response = http.request(request) 43 | } 44 | 45 | rescue Exception # should really list all the possible http exceptions 46 | puts "Attempt: #{tryAgain} Service Unavailable" 47 | sleep (30) 48 | retry if (tryAgain += 1) < @serviceTimeout 49 | end 50 | 51 | response.code 52 | if response.code == "200" # Check the status code anything other than 200 indicates the service is not ready. 53 | puts "Attempt: #{tryAgain} #{response.code} The Nexpose Service appears to be up and functional" 54 | tryAgain = @serviceTimeout 55 | else 56 | puts "Attempt: #{tryAgain} #{response.code} #{response.message} The Service is not yet fully initialized" 57 | tryAgain += 1 58 | sleep(30) 59 | end 60 | end while tryAgain < @serviceTimeout 61 | 62 | if (response.code != "200") 63 | puts "The service was never determined to be available. Action Timed Out" 64 | exit 65 | end 66 | end 67 | 68 | 69 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 70 | 71 | 72 | begin 73 | checkService() 74 | puts 'logging into Nexpose' 75 | nsc.login 76 | rescue ::Nexpose::APIError => err 77 | $stderr.puts("Connection failed: #{e.reason}") 78 | exit(1) 79 | end 80 | 81 | puts 'logged into Nexpose' 82 | at_exit { nsc.logout } 83 | 84 | begin 85 | # Check scan activity wait until there are no scans running 86 | active_scans = nsc.scan_activity 87 | if active_scans.any? 88 | puts "Current scan status: #{active_scans.to_s}" 89 | sleep(15) 90 | end 91 | end while active_scans.any? 92 | 93 | # Start database maintenance 94 | if active_scans.empty? 95 | platform_independent = true 96 | puts "Initiating Database Maintenance tasks" 97 | nsc.db_maintenance(1,1,1) 98 | else 99 | 100 | end 101 | 102 | 103 | puts 'Logging out' 104 | exit 105 | -------------------------------------------------------------------------------- /discoveryCountCSV.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 04.07.2016 4 | 5 | # Put together to try and determine a good way to answer question https://community.rapid7.com/thread/5394 6 | 7 | # Script performs the following tasks 8 | ## 1.) Retrieve a list of available sites from a console. 9 | ## 2.) Retrieve address entries for each site. 10 | ## 3.) Convert address ranges to ip address counts 11 | ## 4.) Provide a total of scanned addresses per site. 12 | ## 5.) Provide a count of live nodes recorded in the last scan for each site 13 | ## 6.) Provide a total count of discoverable addresses for all sites combined. 14 | ## 7.) Provide a total count of active nodes for all sites combined. 15 | 16 | 17 | require 'yaml' 18 | require 'nexpose' 19 | require 'ipaddr' 20 | require 'pp' 21 | include Nexpose 22 | 23 | # Default Values 24 | 25 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 26 | config = YAML.load_file(config_path) 27 | 28 | @host = config["hostname"] 29 | @userid = config["username"] 30 | @password = config["passwordkey"] 31 | @port = config["port"] 32 | 33 | assetCounter = 0 34 | @liveAssetCounter = 0 35 | @liveNodes = 0 36 | 37 | defaultFile = 'AssetUsage_' + DateTime.now.strftime('%Y-%m-%d--%H%M') + '.csv' 38 | 39 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 40 | #puts 'logging into Nexpose' 41 | 42 | 43 | begin 44 | nsc.login 45 | rescue ::Nexpose::APIError => err 46 | $stderr.puts("Connection failed: #{err.reason}") 47 | exit(1) 48 | end 49 | 50 | at_exit { nsc.logout } 51 | 52 | site = nsc.list_sites 53 | case site.length 54 | when 0 55 | puts("There are currently no active sites on this NeXpose instance") 56 | end 57 | 58 | 59 | def convert_ip_range(start_ip, end_ip) 60 | start_ip = IPAddr.new(start_ip) 61 | end_ip = IPAddr.new(end_ip) 62 | 63 | (start_ip..end_ip).map(&:to_s) 64 | end 65 | 66 | File.open(defaultFile, 'w') do |file| 67 | 68 | file.puts "\"Site\",Discovery Count,Nodes detected in last scan" 69 | puts "Site, Discovery Count, Nodes detected in last scan" 70 | 71 | begin 72 | site.each do |site| 73 | site = Nexpose::Site.load(nsc, site.id) 74 | # puts "Getting defined assets for #{site.name}" 75 | 76 | # pp site ## DEBUG 77 | 78 | site.included_addresses.each do |asset| 79 | @siteCount = 0 80 | @liveNodes = 0 81 | currentCount = 0 82 | 83 | if asset.respond_to? :from 84 | 85 | if asset.to != nil 86 | startRange = "#{asset.from}" if asset.to 87 | endRange = "#{asset.to}" 88 | currentCount = convert_ip_range(startRange.to_s, endRange.to_s).count 89 | else 90 | currentCount = 1 91 | end 92 | end 93 | assetCounter += currentCount 94 | @siteCount += currentCount 95 | end 96 | 97 | latest = nsc.last_scan(site.id) 98 | 99 | if latest 100 | @liveNodes += latest.nodes.live 101 | # @liveNodes += latest.nodes.dead 102 | # @liveNodes += latest.nodes.filtered 103 | # @liveNodes += latest.nodes.unresolved 104 | # @liveNodes += latest.nodes.other 105 | end 106 | 107 | @liveAssetCounter += @liveNodes 108 | file.puts "\"#{site.name}\",#{@siteCount},#{@liveNodes}" 109 | puts "\"#{site.name}\", #{@siteCount}, #{@liveNodes}" 110 | end 111 | end 112 | 113 | file.puts "\"Total tally of discoverable addresses for all sites:\",#{assetCounter}" 114 | puts "\"Total tally of discoverable addresses for all sites:\",#{assetCounter}" 115 | file.puts "\"Total tally of live nodes for all sites:\",#{@liveAssetCounter}" 116 | puts "\"Total tally of live nodes for all sites:\",#{@liveAssetCounter}" 117 | end 118 | 119 | puts 'Logging out' 120 | exit 121 | -------------------------------------------------------------------------------- /updateEmailAlerts.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # Original creation date 07.25.2014 4 | # 5 | # The bulk of email modification code is adopted from gschneider https://gist.github.com/gschneider-r7/cccbef2293c007122f58 6 | # 7 | 8 | 9 | ## Script performs the following 10 | ## 1.) Parses all sites for SMTP alerts 11 | ## 2.) Finds SMTP alerts that contain a provided email address 12 | ## 3.) Replaces found email address with new email address 13 | 14 | # require gems 15 | require 'yaml' 16 | require 'nexpose' 17 | require 'optparse' 18 | 19 | 20 | # Default Values from yaml file 21 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 22 | config = YAML.load_file(config_path) 23 | 24 | @host = config["hostname"] 25 | @userid = config["username"] 26 | @password = config["passwordkey"] 27 | @port = config["port"] 28 | 29 | 30 | OptionParser.new do |opts| 31 | opts.banner = "Usage: #{File::basename($0)} [options] " 32 | opts.separator '' 33 | opts.separator 'Update an alert email address for each site.' 34 | opts.separator '' 35 | opts.separator 'Options:' 36 | opts.on_tail('--help', 'Print this help message.') { puts opts; exit } 37 | end.parse! 38 | 39 | # Input for old and new email addresses. 40 | unless ARGV[0] and ARGV[1] 41 | $stderr.puts 'The old and new email addresses are required. Use --help for instructions.' 42 | exit(1) 43 | end 44 | 45 | # Assigning the arguments to variables with some assurance that they are proper strings. 46 | oldEmail = ARGV[0].to_s.chomp 47 | newEmail = ARGV[1].to_s.chomp 48 | 49 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 50 | puts 'logging into Nexpose' 51 | 52 | begin 53 | nsc.login 54 | rescue ::Nexpose::APIError => err 55 | $stderr.puts("Connection failed: #{e.reason}") 56 | exit(1) 57 | end 58 | 59 | puts 'logged into Nexpose' 60 | at_exit { nsc.logout } 61 | 62 | # Query the list of sites to work with 63 | sites = nsc.list_sites 64 | 65 | # User notification of changes to be made. 66 | puts "Alerts will be modified to remove #{oldEmail} and add #{newEmail}." 67 | 68 | begin 69 | # Step through each site in the site listing. 70 | sites.each do |site| 71 | begin 72 | # Load the site configuration to make changes 73 | site = Nexpose::Site.load(nsc, site.id) 74 | puts "Evaluating site #{site.name} (id: #{site.id})." 75 | 76 | # Check for configured alerts; skip sites without alerts. 77 | if site.alerts.length > 0 78 | puts "Found #{site.alerts.length} alerts for #{site.name} (id: #{site.id})." 79 | site.alerts.each do |alert| 80 | 81 | # Confirm that the alert is type: SMTPAlert. 82 | # Create a new object from the SMTP alert to make changes 83 | if alert.alert_type.instance_of? Nexpose::SMTPAlert 84 | smtpAlert = alert.alert_type 85 | 86 | # Only edit alerts where the old email address is present. 87 | if smtpAlert.recipients.include?(oldEmail) 88 | smtpAlert.recipients.delete_if { |r| r == oldEmail } 89 | puts "Deleted #{oldEmail} from alert for site #{site.name} (id: #{site.id})." 90 | 91 | # Only update the alert with the email address if it isn't already present 92 | unless smtpAlert.recipients.include?(newEmail) 93 | smtpAlert.add_recipient(newEmail) 94 | puts "Added #{newEmail} to alert for site #{site.name} (id: #{site.id})." 95 | end 96 | 97 | # Commit changes from the new alert object to the existing alert. 98 | # Save the site configuration 99 | alert.alert_type = smtpAlert 100 | site.save(nsc) 101 | puts "Saved changes to site #{site.name} (id:#{site.id})." 102 | else 103 | puts "No changes made to #{site.name} (id: #{site.id})." 104 | end 105 | end 106 | end 107 | end 108 | 109 | # Site level error, continue to the next site. 110 | rescue Exception => e 111 | puts e.message 112 | end 113 | end 114 | 115 | # Global error, this usually exits the loop and terminates. 116 | rescue Exception => e 117 | puts e.message 118 | end 119 | 120 | puts "Updates completed." 121 | exit 122 | -------------------------------------------------------------------------------- /vulnIDQuery.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 05/13/2014 3 | 4 | require 'yaml' 5 | require 'nexpose' 6 | require 'csv' 7 | 8 | # Default Values 9 | 10 | config = YAML.load_file("conf/nexpose.yaml") # From file 11 | 12 | @host = config["hostname"] 13 | @userid = config["username"] 14 | @password = config["passwordkey"] 15 | @port = config["port"] 16 | 17 | 18 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 19 | puts 'logging into Nexpose' 20 | 21 | begin 22 | nsc.login 23 | rescue ::Nexpose::APIError => err 24 | $stderr.puts("Connection failed: #{e.reason}") 25 | exit(1) 26 | end 27 | 28 | puts 'logged into Nexpose' 29 | at_exit { nsc.logout } 30 | 31 | 32 | puts "Example: Enter \"http-openssl-cve-2014-0160\" if you want to query for HeartBleed." 33 | prompt = 'Please enter Vuln-ID to search: ' 34 | print prompt 35 | #Limiting character count to 32 36 | UserInput = STDIN.gets(32).chomp() 37 | 38 | 39 | 40 | sqlSelect = " 41 | WITH 42 | asset_ips AS ( 43 | SELECT asset_id, ip_address, type 44 | FROM dim_asset_ip_address dips 45 | ), 46 | asset_addresses AS ( 47 | SELECT da.asset_id, 48 | (SELECT array_to_string(array_agg(ip_address), ',') FROM asset_ips WHERE asset_id = da.asset_id AND type = 'IPv4') AS ipv4s, 49 | (SELECT array_to_string(array_agg(ip_address), ',') FROM asset_ips WHERE asset_id = da.asset_id AND type = 'IPv6') AS ipv6s, 50 | (SELECT array_to_string(array_agg(mac_address), ',') FROM dim_asset_mac_address WHERE asset_id = da.asset_id) AS macs 51 | FROM dim_asset da 52 | JOIN asset_ips USING (asset_id) 53 | ), 54 | asset_names AS ( 55 | SELECT asset_id, array_to_string(array_agg(host_name), ',') AS names 56 | FROM dim_asset_host_name 57 | GROUP BY asset_id 58 | ), 59 | asset_facts AS ( 60 | SELECT asset_id, riskscore, exploits, malware_kits 61 | FROM fact_asset 62 | ), 63 | vulnerability_metadata AS ( 64 | SELECT * 65 | FROM dim_vulnerability dv 66 | ), 67 | vuln_cves_ids AS ( 68 | SELECT vulnerability_id, array_to_string(array_agg(reference), ',') AS cves 69 | FROM dim_vulnerability_reference 70 | WHERE source = 'CVE' 71 | GROUP BY vulnerability_id 72 | ) 73 | 74 | 75 | SELECT 76 | da.ip_address AS \"Asset IP Address\", 77 | favi.port AS \"Service Port\", 78 | dp.name AS \"Service Protocol\", 79 | dsvc.name AS \"Service Name\", 80 | an.names AS \"Asset Names\", 81 | favi.date AS \"Vulnerability Test Date\", 82 | dsc.started AS \"Last Scan Time\", 83 | favi.scan_id AS \"Scan ID\", 84 | ds.name AS \"Site Name\", 85 | ds.importance AS \"Site Importance\", 86 | vm.date_published AS \"Vulnerability Published Date\", 87 | ROUND((EXTRACT(epoch FROM age(now(), date_published)) / (60 * 60 * 24))::numeric, 0) AS \"Vulnerability Age\", 88 | cves.cves AS \"Vulnerability CVE IDs\", 89 | vm.title AS \"Vulnerability Title\", 90 | vm.cvss_score AS \"Vulnerability CVSS Score\", 91 | proofAsText(vm.description) AS \"Vulnerability Description\", 92 | vm.nexpose_id AS \"Vulnerability ID\", 93 | vm.severity AS \"Vulnerability Severity Level\", 94 | dvs.description AS \"Vulnerability Test Result Description\" 95 | 96 | 97 | FROM fact_asset_vulnerability_instance favi 98 | JOIN dim_asset da USING (asset_id) 99 | LEFT OUTER JOIN asset_addresses aa USING (asset_id) 100 | LEFT OUTER JOIN asset_names an USING (asset_id) 101 | JOIN asset_facts af USING (asset_id) 102 | JOIN dim_service dsvc USING (service_id) 103 | JOIN dim_protocol dp USING (protocol_id) 104 | JOIN dim_site_asset dsa USING (asset_id) 105 | JOIN dim_site ds USING (site_id) 106 | JOIN vulnerability_metadata vm USING (vulnerability_id) 107 | JOIN dim_vulnerability_status dvs USING (status_id) 108 | JOIN dim_operating_system dos USING (operating_system_id) 109 | LEFT OUTER JOIN dim_scan dsc USING (scan_id) 110 | LEFT OUTER JOIN vuln_cves_ids cves USING (vulnerability_id) " 111 | 112 | sqlWhere = "WHERE vm.nexpose_id LIKE '%#{UserInput}%';" 113 | 114 | query = sqlSelect + sqlWhere 115 | 116 | 117 | report = Nexpose::AdhocReportConfig.new(nil, 'sql') 118 | report.add_filter('version', '1.2.1') 119 | report.add_filter('query', query) 120 | report.add_filter('group', 1) 121 | report_output = report.generate(nsc) 122 | csv_output = CSV.parse(report_output.chomp, { :headers => :first_row }) 123 | CSV.open('nexpose-export.csv', 'w') do |csv_file| 124 | csv_file << csv_output.headers 125 | csv_output.each do |row| 126 | csv_file << row 127 | end 128 | end 129 | # else 130 | # puts "Failure generating report" 131 | # nsc.logout 132 | # exit 1 133 | 134 | 135 | puts 'Report completed and saved to ./nexpose-export.csv.' 136 | 137 | exit 138 | -------------------------------------------------------------------------------- /assetGroupQuery.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 08/08/2014 3 | 4 | 5 | # Queries heavily draw from: 6 | # https://community.rapid7.com/message/11358#11358 7 | # https://community.rapid7.com/docs/DOC-2612 8 | # 9 | 10 | 11 | 12 | require 'yaml' 13 | require 'nexpose' 14 | require 'csv' 15 | 16 | # Default Values 17 | 18 | config = YAML.load_file("conf/nexpose.yaml") # From file 19 | 20 | @host = config["hostname"] 21 | @userid = config["username"] 22 | @password = config["passwordkey"] 23 | @port = config["port"] 24 | 25 | 26 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 27 | puts 'logging into Nexpose' 28 | 29 | begin 30 | nsc.login 31 | rescue ::Nexpose::APIError => err 32 | $stderr.puts("Connection failed: #{e.reason}") 33 | exit(1) 34 | end 35 | 36 | puts 'logged into Nexpose' 37 | at_exit { nsc.logout } 38 | 39 | 40 | puts "Example: Enter \"1\" if you want to query for asset groupid 1." 41 | prompt = 'Please enter an Asset groupid to export a vulnerability list for: ' 42 | print prompt 43 | #Limiting character count to 32 44 | UserInput = STDIN.gets(32).chomp() 45 | 46 | 47 | 48 | sqlSelect = " 49 | WITH 50 | asset_ips AS ( 51 | SELECT asset_id, ip_address, type 52 | FROM dim_asset_ip_address dips 53 | ), 54 | asset_addresses AS ( 55 | SELECT da.asset_id, 56 | (SELECT array_to_string(array_agg(ip_address), ',') FROM asset_ips WHERE asset_id = da.asset_id AND type = 'IPv4') AS ipv4s, 57 | (SELECT array_to_string(array_agg(ip_address), ',') FROM asset_ips WHERE asset_id = da.asset_id AND type = 'IPv6') AS ipv6s, 58 | (SELECT array_to_string(array_agg(mac_address), ',') FROM dim_asset_mac_address WHERE asset_id = da.asset_id) AS macs 59 | FROM dim_asset da 60 | JOIN asset_ips USING (asset_id) 61 | ), 62 | asset_names AS ( 63 | SELECT asset_id, array_to_string(array_agg(host_name), ',') AS names 64 | FROM dim_asset_host_name 65 | GROUP BY asset_id 66 | ), 67 | asset_facts AS ( 68 | SELECT asset_id, riskscore, exploits, malware_kits 69 | FROM fact_asset 70 | ), 71 | vulnerability_metadata AS ( 72 | SELECT * 73 | FROM dim_vulnerability dv 74 | ), 75 | vuln_cves_ids AS ( 76 | SELECT vulnerability_id, array_to_string(array_agg(reference), ',') AS cves 77 | FROM dim_vulnerability_reference 78 | GROUP BY vulnerability_id 79 | ) 80 | 81 | 82 | SELECT 83 | da.ip_address AS \"Asset IP Address\", 84 | favi.port AS \"Service Port\", 85 | dp.name AS \"Service Protocol\", 86 | dsvc.name AS \"Service Name\", 87 | an.names AS \"Asset Names\", 88 | favi.date AS \"Vulnerability Test Date\", 89 | dsc.started AS \"Last Scan Time\", 90 | favi.scan_id AS \"Scan ID\", 91 | ds.name AS \"Site Name\", 92 | ds.importance AS \"Site Importance\", 93 | vm.date_published AS \"Vulnerability Published Date\", 94 | ROUND((EXTRACT(epoch FROM age(now(), date_published)) / (60 * 60 * 24))::numeric, 0) AS \"Vulnerability Age\", 95 | cves.cves AS \"Vulnerability CVE IDs\", 96 | vm.title AS \"Vulnerability Title\", 97 | vm.cvss_score AS \"Vulnerability CVSS Score\", 98 | proofAsText(vm.description) AS \"Vulnerability Description\", 99 | vm.nexpose_id AS \"Vulnerability ID\", 100 | vm.severity AS \"Vulnerability Severity Level\", 101 | dvs.description AS \"Vulnerability Test Result Description\", 102 | dag.name AS \"Asset Group\" 103 | 104 | FROM fact_asset_vulnerability_instance favi 105 | JOIN dim_asset da USING (asset_id) 106 | LEFT OUTER JOIN asset_addresses aa USING (asset_id) 107 | LEFT OUTER JOIN asset_names an USING (asset_id) 108 | JOIN asset_facts af USING (asset_id) 109 | JOIN dim_service dsvc USING (service_id) 110 | JOIN dim_protocol dp USING (protocol_id) 111 | JOIN dim_site_asset dsa USING (asset_id) 112 | JOIN dim_asset_group_asset USING (asset_id) 113 | JOIN dim_asset_group dag USING (asset_group_id) 114 | JOIN dim_site ds USING (site_id) 115 | JOIN vulnerability_metadata vm USING (vulnerability_id) 116 | JOIN dim_vulnerability_status dvs USING (status_id) 117 | JOIN dim_operating_system dos USING (operating_system_id) 118 | LEFT OUTER JOIN dim_scan dsc USING (scan_id) 119 | LEFT OUTER JOIN vuln_cves_ids cves USING (vulnerability_id) " 120 | 121 | sqlWhere = "WHERE asset_group_id = '#{UserInput}';" 122 | 123 | query = sqlSelect + sqlWhere 124 | 125 | 126 | report = Nexpose::AdhocReportConfig.new(nil, 'sql') 127 | report.add_filter('version', '1.2.1') 128 | report.add_filter('query', query) 129 | report_output = report.generate(nsc,18000) # Timeout for report generation is currently set at ~30 minutes 130 | csv_output = CSV.parse(report_output.chomp, { :headers => :first_row }) 131 | CSV.open("AssetGroup_#{UserInput}_export.csv", 'w') do |csv_file| 132 | csv_file << csv_output.headers 133 | csv_output.each do |row| 134 | csv_file << row 135 | end 136 | end 137 | 138 | puts "CSV export completed and saved to ./AssetGroup_#{UserInput}_export.csv." 139 | 140 | exit 141 | -------------------------------------------------------------------------------- /massMaxDurationMod.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # Original creation date 08.26.2015 4 | 5 | ## Script performs the following 6 | ## 1.) Parses all sites 7 | ## 2.) Itterates all available scan schedules for each site 8 | ## 3.) Modifies existing max scan duration times to a new default time 9 | 10 | ## ToDo:) 11 | # 1.) Output a csv log of changes. 12 | # 2.) Accept a csv of sites to change and the values to be used for each specified siteID. 13 | 14 | # require gems 15 | require 'yaml' 16 | require 'nexpose' 17 | require 'pp' 18 | 19 | include Nexpose 20 | 21 | # Default Values from yaml file 22 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 23 | config = YAML.load_file(config_path) 24 | 25 | # Console login configurations 26 | @host = config["hostname"] 27 | @userid = config["username"] 28 | @password = config["passwordkey"] 29 | @port = config["port"] 30 | 31 | 32 | # Configure Options for alert template 33 | 34 | ## Define a new Max Scan Duration time to use for all scans as a default 35 | defaultMaxDuration = 1440 36 | 37 | ## Initialize connection timeout values. 38 | ## Timeout example provided by JGreen in https://community.rapid7.com/thread/5075 39 | 40 | module Nexpose 41 | class APIRequest 42 | include XMLUtils 43 | # Execute an API request 44 | def self.execute(url, req, api_version='2.0', options = {}) 45 | options = {timeout: @nexposeAjaxTimeout} 46 | obj = self.new(req.to_s, url, api_version) 47 | obj.execute(options) 48 | return obj 49 | end 50 | end 51 | 52 | 53 | module AJAX 54 | def self._https(nsc) 55 | http = Net::HTTP.new(nsc.host, nsc.port) 56 | http.use_ssl = true 57 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 58 | http.read_timeout = @nexposeAjaxTimeout 59 | http 60 | end 61 | end 62 | end 63 | 64 | 65 | 66 | 67 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 68 | begin 69 | nsc.login 70 | rescue ::Nexpose::APIError => err 71 | $stderr.puts("Connection failed: #{err.reason}") 72 | exit(1) 73 | end 74 | 75 | puts 'logged into Nexpose' 76 | at_exit { nsc.logout } 77 | 78 | puts "Changes may not be made while there are active or paused scans in the console" 79 | 80 | begin 81 | begin 82 | puts "Requesting scan status updates from #{@host}\r\n" 83 | ## Pull data for paused scans - Method suggested by JGreen https://community.rapid7.com/thread/5075 (THANKS!!!) 84 | pausedScans = DataTable._get_dyn_table(nsc, '/data/site/scans/dyntable.xml?printDocType=0&tableID=siteScansTable&activeOnly=true').select { |scanHistory| (scanHistory['Status'].include? 'Paused')} 85 | 86 | # Check scan activity wait until there are no scans running or paused 87 | activeScans = nsc.scan_activity() 88 | 89 | puts "Active Scans: #{activeScans.count}" 90 | puts "Paused Scans: #{pausedScans.count}" 91 | 92 | if (activeScans.any? or pausedScans.any?) 93 | puts " Trying again in 60 seconds" 94 | sleep (60) 95 | end 96 | rescue Exception => err 97 | puts err.message # should really list all the possible http exceptions 98 | exit 99 | end 100 | end while (activeScans.any? or pausedScans.any?) 101 | 102 | 103 | if activeScans.empty? 104 | 105 | # Query the list of sites to work with 106 | sites = nsc.list_sites 107 | 108 | # User notification of changes to be made. 109 | puts "Maximum scan duration times will be added or modified for every site located on this console." 110 | 111 | begin 112 | # Step through each site in the site listing. 113 | sites.each do |eachSite| 114 | begin 115 | # Load the site configuration to make changes 116 | site = Nexpose::Site.load(nsc, eachSite.id) 117 | puts "Evaluating site #{site.name} (id: #{site.id})." 118 | 119 | 120 | begin 121 | # Check for existing scheduled scans within the site 122 | if site.schedules.length > 0 123 | puts "Number of scheduled scans for #{site.name} (id: #{site.id}): #{site.schedules.length} " 124 | 125 | site.schedules.each do |scheduledScan| 126 | 127 | puts "Current max_duration: #{scheduledScan.max_duration}" 128 | scheduledScan.max_duration = defaultMaxDuration 129 | puts "Modified max_duration: #{scheduledScan.max_duration}" 130 | 131 | begin 132 | 133 | # Finalize changes 134 | 135 | # Save the site configuration with the modified scan schedule value 136 | puts " Saving the new duration value for the scheduled scan." 137 | site.save(nsc) 138 | puts "Changes saved to site #{site.name} (id:#{site.id})." 139 | 140 | # Site schedule level error, continue to the next schedule. 141 | rescue Exception => err 142 | puts err.message 143 | end 144 | 145 | 146 | end 147 | end 148 | end 149 | # Site level error, continue to the next site. 150 | rescue Exception => err 151 | puts err.message 152 | end 153 | 154 | end 155 | 156 | # Global error, this usually exits the loop and terminates. 157 | rescue Exception => err 158 | puts err.message 159 | exit 160 | end 161 | 162 | else 163 | 164 | end 165 | 166 | puts "Updates completed." 167 | exit 168 | -------------------------------------------------------------------------------- /createSyslogAlerts.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # Original creation date 04.28.2015 4 | 5 | ## Script performs the following 6 | ## 1.) Parses all sites and adds a syslog alert 7 | ## 2.) Looks for existing alerts in each site with the same name as the new alert and removes them prior to adding the new alert. 8 | 9 | 10 | # require gems 11 | require 'yaml' 12 | require 'nexpose' 13 | require 'pp' 14 | 15 | 16 | # Default Values from yaml file 17 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 18 | config = YAML.load_file(config_path) 19 | 20 | # Console login configurations 21 | @host = config["hostname"] 22 | @userid = config["username"] 23 | @password = config["passwordkey"] 24 | @port = config["port"] 25 | 26 | 27 | # Configure Options for alert template 28 | 29 | ## Alert Name Prefix 30 | alertPrefix = "SysLog_SiteID_" 31 | 32 | ## Log server / port to use in alerts 33 | logServer = config["logserver"] 34 | logPort = config["logport"] 35 | 36 | ## Scan alert filters 37 | alertFail = config["alertFail"] 38 | alertPause = config["alertPause"] 39 | alertResume = config["alertResume"] 40 | alertStart = config["alertStart"] 41 | alertStop = config["alertStop"] 42 | 43 | ## Vuln alert filters 44 | alertConfirmed = config["alertConfirmed"] 45 | alertPotential = config["alertPotential"] 46 | alertSeverity = config["alertSeverity"] 47 | alertUnconfirmed = config["alertUnconfirmed"] 48 | 49 | 50 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 51 | begin 52 | nsc.login 53 | rescue ::Nexpose::APIError => err 54 | $stderr.puts("Connection failed: #{err.reason}") 55 | exit(1) 56 | end 57 | 58 | puts 'logged into Nexpose' 59 | at_exit { nsc.logout } 60 | 61 | # Query the list of sites to work with 62 | sites = nsc.list_sites 63 | 64 | # User notification of changes to be made. 65 | puts "Syslog alerts will be added to every site located on this console." 66 | 67 | begin 68 | # Step through each site in the site listing. 69 | sites.each do |eachSite| 70 | begin 71 | # Load the site configuration to make changes 72 | site = Nexpose::Site.load(nsc, eachSite.id) 73 | puts "Evaluating site #{site.name} (id: #{site.id})." 74 | # Check for an existing alert within the site with the configured pre-fix #{alertPrefix} 75 | if site.alerts.length > 0 76 | puts "Found #{site.alerts.length} alerts for #{site.name} (id: #{site.id})." 77 | 78 | site.alerts.each do |alert| 79 | 80 | # Remove old syslog alerts with the same alert name. 81 | if alert.name.include?("#{alertPrefix}#{site.id}") 82 | # Create a new object from the alerts to make changes 83 | if alert.alert_type.include?("Syslog") 84 | if alert.name.include?("#{alertPrefix}#{site.id}") 85 | site.alerts.delete_if{ |obj| obj.name.include?("#{alertPrefix}#{site.id}")} 86 | puts "Deleted #{alertPrefix}#{site.id} from alerts for site #{site.name} (id: #{site.id})." 87 | 88 | # Save the site configuration 89 | site.save(nsc) 90 | puts "Saved changes to site #{site.name} (id:#{site.id})." 91 | end 92 | end 93 | end 94 | end 95 | end 96 | rescue Exception => err 97 | puts err.message 98 | end 99 | 100 | begin 101 | # Initialize a syslog alert for the site. 102 | syslogAlert = Nexpose::SyslogAlert.new("#{alertPrefix}#{site.id}", nsc, 1, -1) 103 | puts "Initiated adding #{logServer} to alert for site #{site.name} (id: #{site.id})." 104 | 105 | # Set the syslog server to use. 106 | puts " Setting #{logServer} as the log receiver" 107 | syslogAlert.server = logServer # Defined in ./conf/nexpose.yaml 108 | 109 | # Set the syslog port to use 110 | puts " Setting logging port #{logPort} for the log receiver" 111 | syslogAlert.server_port = logPort # Defined in ./conf/nexpose.yaml 112 | 113 | puts " Setting the scan filters" 114 | alertScanFilter = Nexpose::ScanFilter.new # setup scan filter? 115 | 116 | ## Scan alert filters 117 | alertScanFilter.fail = alertFail 118 | puts " Fail = #{alertFail}" 119 | alertScanFilter.pause = alertPause 120 | puts " Pause = #{alertPause}" 121 | alertScanFilter.resume = alertResume 122 | puts " Resume = #{alertResume}" 123 | alertScanFilter.start = alertStart 124 | puts " Start = #{alertStart}" 125 | alertScanFilter.stop = alertStop 126 | puts " Stop = #{alertStop}" 127 | 128 | # Assign filters to scan_filter 129 | syslogAlert.scan_filter = alertScanFilter 130 | 131 | puts " Setting the vuln filter" 132 | alertVulnFilter = Nexpose::VulnFilter.new # setup vuln filter? 133 | 134 | ## Vuln alert filters 135 | alertVulnFilter.confirmed = alertConfirmed 136 | puts " Confirmed = #{alertConfirmed}" 137 | alertVulnFilter.potential = alertPotential 138 | puts " Potential = #{alertPotential}" 139 | alertVulnFilter.severity = alertSeverity 140 | puts " Severity = #{alertSeverity}" 141 | alertVulnFilter.unconfirmed = alertUnconfirmed 142 | puts " Unconfirmed = #{alertUnconfirmed}" 143 | 144 | # Assign filters to vuln_filter 145 | syslogAlert.vuln_filter = alertVulnFilter 146 | 147 | # Apply alert to site. 148 | site.alerts << syslogAlert 149 | 150 | # Save the site configuration 151 | puts " Saving the alert" 152 | site.save(nsc) 153 | puts "Changes saved to site #{site.name} (id:#{site.id})." 154 | 155 | # Site level error, continue to the next site. 156 | rescue Exception => err 157 | puts err.message 158 | end 159 | end 160 | 161 | # Global error, this usually exits the loop and terminates. 162 | rescue Exception => err 163 | puts err.message 164 | end 165 | 166 | puts "Updates completed." 167 | exit 168 | -------------------------------------------------------------------------------- /createEmailAlerts.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # https://github.com/BrianWGray 4 | # Original creation date 08.22.2017 5 | 6 | ## Script performs the following 7 | ## 1.) Parses all sites and adds an smtp alert 8 | ## 2.) Looks for existing alerts in each site with the same name as the new alert and removes them prior to adding the new alert. 9 | 10 | # require gems 11 | require 'yaml' 12 | require 'nexpose' 13 | require 'pp' 14 | 15 | # Default Values from yaml file 16 | # The values may be configured manually, this is for the script creators benefit. 17 | # An example configuration file is available in the source git repository 18 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 19 | config = YAML.load_file(config_path) 20 | 21 | # Console login configurations 22 | @host = config["hostname"] 23 | @userid = config["username"] 24 | @password = config["passwordkey"] 25 | @port = config["port"] 26 | 27 | # Configure Options for alert template 28 | 29 | ## Alert Name Prefix 30 | alertPrefix = "SMTPAlert_SiteID_" 31 | 32 | ## SMTP server / port to use in alerts 33 | smtpServer = "localhost" #config["smtpserver"] 34 | smtpPort = 25 #config["smtpport"] # Not currently used 35 | 36 | # Mail Info 37 | sender = "vulnsender@example.com" #config["smtpsender"] 38 | recipients = ["recipients@example.com"] #config["smtprecipients"] # must be in array form. 39 | 40 | ## Scan alert filters 41 | alertFail = 1 #config["alertFail"] 42 | alertPause = 1 #config["alertPause"] 43 | alertResume = 1 #config["alertResume"] 44 | alertStart = 1 #config["alertStart"] 45 | alertStop = 1 #config["alertStop"] 46 | 47 | ## Vuln alert filters 48 | alertConfirmed = 1 #config["alertConfirmed"] 49 | alertPotential = 1 #config["alertPotential"] 50 | alertSeverity = 1 #config["alertSeverity"] 51 | alertUnconfirmed = 1 #config["alertUnconfirmed"] 52 | 53 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 54 | begin 55 | nsc.login 56 | rescue ::Nexpose::APIError => err 57 | $stderr.puts("Connection failed: #{err.reason}") 58 | exit(1) 59 | end 60 | at_exit { nsc.logout } 61 | 62 | # Query the list of sites to work with 63 | sites = nsc.list_sites 64 | 65 | # User notification of changes to be made. 66 | puts "SMTP alerts will be added to every site located on this console." 67 | 68 | begin 69 | # Step through each site in the site listing. 70 | sites.each do |eachSite| 71 | begin 72 | # Load the site configuration to make changes 73 | site = Nexpose::Site.load(nsc, eachSite.id) 74 | puts "\nEvaluating site #{site.name} (id: #{site.id})." 75 | # Check for an existing alert within the site with the configured pre-fix #{alertPrefix} 76 | if site.alerts.length > 0 77 | puts "Found #{site.alerts.length} alerts for #{site.name} (id: #{site.id})." 78 | 79 | site.alerts.each do |alert| 80 | # Remove old alerts with the same alert name. 81 | if alert.alert_type.include?("SMTP") 82 | # Create a new object from the alerts to make changes 83 | if alert.alert_type.include?("SMTP") 84 | if alert.name.include?("#{alertPrefix}#{site.id}") 85 | site.alerts.delete_if{ |obj| obj.alert_type.include?("SMTP")} 86 | puts "Deleted #{alert.name} from alerts for site #{site.name} (id: #{site.id})." 87 | 88 | # Save the site configuration 89 | site.save(nsc) 90 | puts "Saved changes to site #{site.name} (id:#{site.id})." 91 | end 92 | end 93 | end 94 | end 95 | end 96 | rescue Exception => err 97 | puts err.message 98 | end 99 | 100 | begin 101 | # Initialize an alert for the site. 102 | smtpAlert = Nexpose::SMTPAlert.new("#{alertPrefix}#{site.id}", sender, smtpServer, recipients, 1, -1, 1) 103 | 104 | puts "Initiated adding #{alertPrefix}#{site.id} alert for site #{site.name} (id: #{site.id})." 105 | puts " Set alert mail recipients #{recipients} for alerts" 106 | puts " Set sender address #{sender} for alerts" 107 | puts " Set SMTP server #{smtpServer} for alerts" 108 | puts " Setting the scan filters" 109 | alertScanFilter = Nexpose::ScanFilter.new # setup scan filter? 110 | 111 | ## Scan alert filters 112 | alertScanFilter.fail = alertFail 113 | puts " Fail = #{alertFail}" 114 | alertScanFilter.pause = alertPause 115 | puts " Pause = #{alertPause}" 116 | alertScanFilter.resume = alertResume 117 | puts " Resume = #{alertResume}" 118 | alertScanFilter.start = alertStart 119 | puts " Start = #{alertStart}" 120 | alertScanFilter.stop = alertStop 121 | puts " Stop = #{alertStop}" 122 | 123 | # Assign filters to scan_filter 124 | smtpAlert.scan_filter = alertScanFilter 125 | 126 | puts " Setting the vuln filter" 127 | alertVulnFilter = Nexpose::VulnFilter.new # setup vuln filter? 128 | 129 | ## Vuln alert filters 130 | alertVulnFilter.confirmed = alertConfirmed 131 | puts " Confirmed = #{alertConfirmed}" 132 | alertVulnFilter.potential = alertPotential 133 | puts " Potential = #{alertPotential}" 134 | alertVulnFilter.severity = alertSeverity 135 | puts " Severity = #{alertSeverity}" 136 | alertVulnFilter.unconfirmed = alertUnconfirmed 137 | puts " Unconfirmed = #{alertUnconfirmed}" 138 | 139 | # Assign filters to vuln_filter 140 | smtpAlert.vuln_filter = alertVulnFilter 141 | 142 | # Save the alert configuration 143 | puts " Saving the alert" 144 | smtpAlert.save(nsc, site.id) 145 | 146 | puts "Alert changes saved to site #{site.name} (id:#{site.id})." 147 | 148 | # Site level error, continue to the next site. 149 | rescue Exception => err 150 | puts err.message 151 | end 152 | end 153 | 154 | # Global error, this usually exits the loop and terminates. 155 | rescue Exception => err 156 | puts err.message 157 | end 158 | 159 | puts "Updates completed." 160 | exit 161 | 162 | 163 | -------------------------------------------------------------------------------- /calGen.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 12.23.2014 4 | 5 | 6 | # This script generates an importable .ics file for scan schedules. 7 | # 8 | 9 | # Dependencies required to be installed: 10 | # sudo gem install icalendar 11 | # sudo gem install nexpose 12 | # sudo gem install yaml 13 | # for an example ./conf/nexpose.yaml see https://github.com/BrianWGray/nexpose/blob/master/conf/nexpose.yaml 14 | 15 | # misterpaul's scanPlanReporter.rb script was the catalyst for this script. 16 | # https://github.com/misterpaul/NexposeRubyScripts/tree/master/ScanPlanReporter 17 | 18 | 19 | require 'yaml' 20 | require 'icalendar' 21 | require 'nexpose' 22 | require 'time' 23 | include Nexpose 24 | 25 | # Default Values from yaml file 26 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 27 | config = YAML.load_file(config_path) 28 | 29 | @host = config["hostname"] 30 | @userid = config["username"] 31 | @password = config["passwordkey"] 32 | @port = config["port"] 33 | @icsfilename = config["icsfilename"] 34 | @icsiterations = config["icsitterations"] 35 | 36 | 37 | # Output filename 38 | output_fn = @icsfilename.to_s 39 | 40 | # Number of scan recurrences for the .ics file to include. 41 | numIterations = @icsiterations.to_i 42 | 43 | begin 44 | @nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 45 | puts 'logging into Nexpose' 46 | 47 | begin 48 | @nsc.login 49 | rescue ::Nexpose::APIError => err 50 | $stderr.puts("Connection failed: #{err.reason}") 51 | exit(1) 52 | end 53 | 54 | puts 'logged into Nexpose' 55 | at_exit { @nsc.logout } 56 | 57 | sites = @nsc.sites 58 | 59 | # Create calendar 60 | cal = Icalendar::Calendar.new 61 | 62 | # Check engine status. 63 | @nsc.console_command('version engines') 64 | engine_list = {} 65 | @nsc.engines.each do |engine| 66 | engine_list[engine.id] = "#{engine.name} (#{engine.status})" 67 | end 68 | 69 | # 'Site Name,Last Scan Start,Last Scan Live Nodes,Scan Template,Scan Engine,Next Scan Start,Schedule' 70 | 71 | sites.each do |s| 72 | 73 | latest_start_time = status = active = duration = engine_name = 'na' 74 | 75 | site = Site.load(@nsc, s.id) 76 | puts "Pulling Scan data for site: #{site.id}\tname: #{site.name}" 77 | template = site.scan_template 78 | 79 | latest = @nsc.last_scan(site.id) 80 | if latest 81 | latest_start_time = latest.start_time 82 | latest_end_time = latest.end_time # We initially set the end_time to the amount of time the last scan took. 83 | end_time = latest_end_time 84 | active = latest.nodes.live 85 | engine_name = engine_list[site.engine] 86 | if sched = site.schedules.first 87 | schedule = "#{sched.type}:#{sched.interval}" 88 | if sched.max_duration 89 | # If we find a max time defined we use it as the end_time to specify the available scan window 90 | # this replaces the guess created from the last scan duration. 91 | maxDuration = sched.max_duration 92 | else 93 | maxDuration = 0 94 | end 95 | end 96 | 97 | if latest.end_time 98 | duration_sec = latest.end_time - latest_start_time 99 | hours = (duration_sec / 3600).to_i 100 | minutes = (duration_sec / 60 - hours * 60).to_i 101 | seconds = (duration_sec - (minutes * 60 + hours * 3600)) 102 | duration = sprintf('%dh %02dm %02ds', hours, minutes, seconds) 103 | else 104 | duration = 'na' 105 | end 106 | 107 | if defined? sched.enabled # Check to see if the site has a schedule enabled. 108 | 109 | 110 | start_time = Time.parse(sched.start) 111 | end_time = start_time + maxDuration*60 112 | 113 | puts "Site:#{site.name} starts #{start_time} Max scan time: #{maxDuration} #{end_time} using #{template} from #{engine_name} on a #{sched.type.upcase} schedule Interval: #{sched.interval}" 114 | 115 | event = cal.event 116 | 117 | event.summary = "Nexpose Scan for site: #{site.name}" 118 | event.dtstart = DateTime.parse("#{start_time}") 119 | event.dtend = DateTime.parse("#{end_time}") 120 | event.location = "#{engine_name} scanning #{site.name} with template #{template}" 121 | event.description = "Site:#{site.name} is scheduled to be scanned by #{engine_name} starting at #{start_time} with an expected end time of #{end_time} and a Max scan time of #{maxDuration} minutes. The scan will be completed using scan template: #{template}. Scans for this site have taken ~ #{duration} in the past. This scan is part of a schedule to scan this site with a #{sched.type} schedule type. https://#{@host}:#{@port}/site.jsp?siteid=#{site.id}" 122 | 123 | # This is intended to generate Recurrence Rules for the icalendar entries based on 124 | # http://www.ietf.org/rfc/rfc2445.txt 125 | # There is still a good bit of work that needs to be done here. 126 | 127 | case 128 | when sched.type == "daily" 129 | event.rrule = ["FREQ=DAILY;INTERVAL=#{sched.interval.to_i};COUNT=numIterations*7"] # Iterations are assumed to be in weeks here. 130 | when sched.type == "weekly" 131 | event.rrule = ["FREQ=WEEKLY;INTERVAL=#{sched.interval.to_i};COUNT=numIterations"] 132 | when sched.type == "monthly-date" 133 | event.rrule = ["FREQ=MONTHLY;INTERVAL=#{sched.interval.to_i};COUNT=numIterations"] 134 | when sched.type == "monthly-day" 135 | # I need to take more time to hash out the best way to implement this. Should be fairly straight forward? 136 | # event.rrule = ["FREQ=MONTHLY;BYMONTHDAY=#{sched.interval.to_i};COUNT=numIterations"] 137 | end 138 | 139 | cal.add_event(event) 140 | 141 | else 142 | puts "No schedule is enabled for this site" 143 | 144 | end 145 | 146 | else 147 | # No scans found. 148 | end 149 | end 150 | 151 | output = File.new(output_fn, 'w') 152 | output.write(cal.to_ical) 153 | puts "iCalendar file #{output_fn} saved" 154 | 155 | end 156 | 157 | -------------------------------------------------------------------------------- /Nexpose-Lieberman-Integration/lieberman_integration.rb: -------------------------------------------------------------------------------- 1 | # Nexpose <-> Lieberman integration script. 2 | # The following gems need to be installed: nexpose 3 | # This script will perform the following steps: 4 | # 1.- Query nexpose for a list of sites. 5 | # 2.- Go site by site and retrieve the assets. 6 | # 3.- For all assets that are hostnames (not ips) it'll query lieberman for passwords. 7 | # 4.- For those that could checkout passwords it'll save back those credentials back into nexpose. 8 | # 5.- Kick a scan of the site if the setting for it below was set to yes. 9 | # For support, please email integrations_support@rapid7.com with the issue and a copy of the log. 10 | 11 | 12 | # SCRIPT CONFIGURATION: 13 | # Nexpose console information. 14 | # Nexpose IP / Hostname. 15 | @console = '192.168.99.190' 16 | 17 | # Nexpose username. 18 | @nxuser = 'nxadmin' 19 | 20 | # Nexpose Password. 21 | @nxpass = 'nxadmin' 22 | 23 | # Start scan after site is updated? 24 | @scan = 'N' 25 | 26 | # LOGGING. 27 | # This script includes a logger, all output will be sent to the file service_now.log in the directory 28 | # where this script is run. 29 | require 'Logger' 30 | $LOG = Logger.new('lieberman.log', 'monthly') 31 | 32 | # Valid log levels: Logger::DEBUG Logger::INFO Logger::WARN Logger::ERROR Logger:FATAL 33 | $LOG.level = Logger::INFO 34 | 35 | class LiebermanIntegration 36 | require "net/http" 37 | require "uri" 38 | require_relative "nexpose_integration" 39 | 40 | # Lieberman Web SDK URL. 41 | @@lieberman_instance = "https://lscerpm-2012/PWCWEB/ClientAgentRequests.asp" 42 | # Lieberman Authenticator domain. 43 | @@lieberman_authenticator = "demo" 44 | # Lieberman username. 45 | @@lieberman_user = "rapid7" 46 | # Lieberman password 47 | @@lieberman_password = "R@,o!D7p@ssw0rd" 48 | 49 | def lieberman_login 50 | $LOG.info "Login to Lieberman." 51 | uri = URI.parse("#{@@lieberman_instance}?Command=Login&Authenticator=#{@@lieberman_authenticator}&LoginUsername=#{@@lieberman_user}&LoginPassword=#{@@lieberman_password}") 52 | http = Net::HTTP.new(uri.host, uri.port) 53 | #http.set_debug_output $stdout 54 | request = Net::HTTP::Get.new(uri.request_uri) 55 | http.use_ssl = true 56 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 57 | response = http.request(request) 58 | token = response.body 59 | raise "Could not connect." if token.nil? 60 | raise "Could not authenticate. Check your credentials." unless token.start_with?('Success') 61 | token_array = token.split(';') 62 | @login_token = token_array[1] 63 | $LOG.info "Obtained Lieberman credentials." 64 | end 65 | 66 | def get_account_info(hostname) 67 | begin 68 | $LOG.info "Searching for account information for #{hostname}." 69 | uri = URI.parse("#{@@lieberman_instance}?Command=ListStoredAccountsForSystem&AuthenticationToken=#{@login_token}&SystemName=#{hostname}") 70 | http = Net::HTTP.new(uri.host, uri.port) 71 | http.use_ssl = true 72 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 73 | #http.set_debug_output $stdout 74 | request = Net::HTTP::Get.new(uri.request_uri) 75 | 76 | response = http.request(request) 77 | token = response.body 78 | begin 79 | if token.start_with?('Success') 80 | token_array = token.split(';') 81 | account_info = token_array[1] 82 | if account_info.include? "Linux" then service = "ssh" 83 | else service = "cifs" 84 | end 85 | namespace = account_info.slice(/^[^\\]*\\/).gsub(/\\$/, '').gsub(/\(.*\)/, '') 86 | account_login = account_info.slice(/\\([^\$]*)\$/).gsub(/\\/, '').gsub(/\$/, '') 87 | $LOG.info "Found account information for #{hostname}." 88 | account = {:hostname => hostname, :realm => namespace, :user => account_login, :service => service} 89 | else 90 | $LOG.info "Could not find account information for #{hostname} in Lieberman." 91 | account 92 | end 93 | rescue Exception 94 | $LOG.info "Could not find account information about #{hostname} continuing with the next account." 95 | account 96 | end 97 | rescue 98 | $LOG.info "Lieberman Login token not set." 99 | end 100 | end 101 | 102 | def get_password_for_system(account) 103 | raise "Account information cannot be null or empty." if account.nil? or account.empty? 104 | begin 105 | realm = account[:realm] 106 | user = account[:user] 107 | hostname = account[:hostname] 108 | service = account[:service] 109 | 110 | uri = URI.parse("#{@@lieberman_instance}?Command=GetPasswordFromStore&AuthenticationToken=#{@login_token}&SystemName=#{hostname}&Namespace=#{realm}&AccountName=#{user}&Comment=Nexpose") 111 | http = Net::HTTP.new(uri.host, uri.port) 112 | http.use_ssl = true 113 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 114 | #http.set_debug_output $stdout 115 | request = Net::HTTP::Get.new(uri.request_uri) 116 | 117 | response = http.request(request) 118 | token = response.body 119 | 120 | if token.start_with?('Success') 121 | $LOG.info "Found password for #{hostname}" 122 | token_array = token.split(';') 123 | pre_password = token_array[1].split('=') 124 | full_account = {:service => service, :realm => realm, :user => user, :hostname => hostname, :password => pre_password[1]} 125 | else 126 | $LOG.info "Could not retrieve password for #{hostname} in Lieberman. Check your permissions." 127 | full_account 128 | end 129 | rescue Exception 130 | $LOG.info "Could not retrieve password for #{hostname}." 131 | full_account 132 | end 133 | end 134 | 135 | end 136 | 137 | 138 | # Connects and login to Lieberman 139 | lieberman = LiebermanIntegration.new 140 | lieberman.lieberman_login 141 | 142 | # Connects to Nexpose. 143 | nexpose = NexposeIntegration.new() 144 | nexpose.connect(@console, @nxuser, @nxpass) 145 | 146 | # Get all the sites in Nexpose. 147 | all_sites = nexpose.get_all_sites 148 | raise "No sites found." if all_sites.empty? or all_sites.nil? 149 | 150 | # Resaves every site with the new credentials. 151 | all_sites.each do |site| 152 | 153 | # Get all the hostnames for the site. 154 | hostnames = nexpose.get_hostnames(site) 155 | next if hostnames.empty? 156 | 157 | all_creds = [] 158 | 159 | # Queries Lieberman for all the credentials for all the hostnames. 160 | hostnames.each do |hostname| 161 | account_info = lieberman.get_account_info(hostname.host.slice(/^[^.]*/)) 162 | next if account_info.nil? or account_info.empty? 163 | fullcred = lieberman.get_password_for_system(account_info) 164 | next if fullcred.nil? or fullcred.empty? 165 | all_creds.push(fullcred) 166 | end 167 | 168 | # Saves the site with the credentials. 169 | $LOG.info "Saving credentials for site-id: #{site.id}" 170 | nexpose.save_all_credentials_for_site(all_creds, site.id) 171 | if @scan == 'Y' 172 | $LOG.info "Starting scan for #{site.id}" 173 | nexpose.start_scan_site site.id 174 | end 175 | end -------------------------------------------------------------------------------- /scan_mailer.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # Initial Creation Date: 9.30.2016 4 | 5 | 6 | # Purpose of this script. 7 | # Written as a POC for: 8 | # https://community.rapid7.com/thread/8990 9 | 10 | # This script queries all scheduled scans on a console and the contact email out of each site 11 | # then sends an email notification about when the scan will occur with a 24 hour advanced notice.. 12 | # 13 | 14 | # Dependencies required to be installed: 15 | # sudo gem install nexpose 16 | # sudo gem install yaml 17 | # for an example ./conf/nexpose.yaml see https://github.com/BrianWGray/nexpose/blob/master/conf/nexpose.yaml 18 | 19 | 20 | require 'yaml' 21 | require 'nexpose' 22 | require 'time' 23 | require 'pp' 24 | require 'net/smtp' 25 | include Nexpose 26 | 27 | # Default Values from yaml file 28 | config_path = File.expand_path("../conf/pgh-nvs-01.yaml", __FILE__) 29 | config = YAML.load_file(config_path) 30 | 31 | @host = config["hostname"] 32 | @userid = config["username"] 33 | @password = config["passwordkey"] 34 | @port = config["port"] 35 | 36 | # If you need to add auth: https://www.tutorialspoint.com/ruby/ruby_sending_email.htm 37 | mailFrom = config["mailFrom"] 38 | mailServer = config["mailServer"] 39 | mailPort = config["mailPort"] 40 | mailDomain = config["mailDomain"] 41 | 42 | # If an organizations contact email is not configured for a site send the notice to the defaultEmail address. 43 | defaultEmail = config["defaultEmail"] 44 | mailTo = defaultEmail 45 | 46 | 47 | # The intent for the mailer times is as follows: 48 | # a mailerRange of 24 hours checks for scheduled scans 24 hours from the time of the script running. 49 | # If the script is run at 11am then the script is looking for scans the next day at 11am. 50 | # The scans won't always be exactly 24 hours from now so we provide a mailerWindow of 60 minutes 51 | # This means that any scheduled scan the next day between 11am and 12 will be in the notify window. 52 | 53 | # Values are in seconds and ultimately handled via epoch values 54 | mailerRange = ((60 * 60) * 24) 55 | mailerWindow = ((60 * 60)) # * 24)# => window of time for scheduled scans to be alerted for 56 | 57 | 58 | # Replace Time.parse(time.to_s).to_s assignments with a time normalizing method - BWG 59 | def normalize_time(time) 60 | begin 61 | time = time if(time.is_a?(Time)) 62 | time = Time.parse("#{time.to_s}") if(!time.is_a?(Time)) 63 | rescue 64 | time = Time.now # Upon failure use the current time value 65 | end 66 | 67 | return time 68 | end 69 | 70 | 71 | def send_notification(mailFrom, mailTo, mailDomain, mailServer, noticeContent) 72 | 73 | # These values don't really need to be re-assigned to local variables but I did it. 74 | @summary = noticeContent[:summary] 75 | @location = noticeContent[:location] 76 | @dtstart = noticeContent[:dtstart] 77 | @template = noticeContent[:template] 78 | @description = noticeContent[:description] 79 | 80 | 81 | # Example Email Notification Template. Modify as needed. Sending HTML email by default because I like it. 82 | message = <Vulnerability Scan Notice 90 |
91 |

The listed email address #{mailTo} is registered as the primary notification list for this notice. 93 |

94 | #{@summary} 95 |
96 | A vulnerability scan is scheduled to run against #{@location} starting #{@dtstart}
97 | The scheduled scan is assigned the following template: #{@template} 98 |

99 |

100 | #{@description} 101 |


102 | If you believe you have received this notice in error please contact help@example.com. 103 |

104 | 105 | 106 | MESSAGE_END 107 | 108 | begin 109 | Net::SMTP.start(mailServer) do |smtp| 110 | smtp.send_message message, mailFrom, mailTo 111 | end 112 | 113 | rescue => err 114 | $stderr.puts("Fail: #{err}") 115 | exit(1) 116 | end 117 | 118 | end 119 | 120 | begin 121 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 122 | 123 | begin 124 | nsc.login 125 | rescue ::Nexpose::APIError => err 126 | $stderr.puts("Connection failed: #{err.reason}") 127 | exit(1) 128 | end 129 | at_exit { nsc.logout } 130 | 131 | sites = nsc.sites 132 | 133 | begin 134 | sites.each do |s| 135 | 136 | latest_start_time = status = active = 'na' 137 | 138 | site = Site.load(nsc, s.id) 139 | # puts "Pulling Scan data for site: #{site.id}\tname: #{site.name}" 140 | 141 | site.schedules.each do |sched| 142 | begin 143 | 144 | schedule = "#{sched.type}:#{sched.interval}" 145 | 146 | if defined? sched.enabled # Check to see if the site has a schedule enabled. 147 | 148 | start_time = normalize_time(sched.next_run_time) # => Time of the Next scheduled scan 149 | currentTime = Time.now.to_i # => Current Time to calculate a time range for which notifications to send. 150 | rangeTime = currentTime + mailerRange 151 | timeRange = (rangeTime)..(rangeTime + mailerWindow) # => Range of time from the script run time to notify for. 152 | 153 | if timeRange === start_time.to_i 154 | 155 | # puts "Site:#{site.name} starts #{start_time} on a #{sched.type.upcase} schedule Interval: #{sched.interval}" 156 | if !site.organization.email.nil? 157 | orgMail = "#{site.organization.email}" 158 | else 159 | orgMail = "#{defaultEmail}" 160 | end 161 | 162 | noticeContent = { 163 | contact: orgMail, 164 | summary: "Vulnerability Scan for site: #{site.name}", 165 | dtstart: "#{start_time}", 166 | location: "#{site.name}", 167 | template: "#{sched.scan_template_id}", 168 | description: "Site: #{site.name} is scheduled to be scanned starting at #{start_time}. This scan is part of a schedule to scan this site with a #{sched.type} schedule type. https://#{@host}:#{@port}/site.jsp?siteid=#{site.id}" 169 | # Additional hash values may be added here to provide more information to the notification template. 170 | } 171 | 172 | # Call the mail function to email this schedule. 173 | send_notification(mailFrom, mailTo, mailDomain, mailServer, noticeContent) 174 | end 175 | end 176 | rescue => err 177 | $stderr.puts("Fail: #{err}") 178 | exit(1) 179 | end 180 | end 181 | end 182 | 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /scanCleanup.rb: -------------------------------------------------------------------------------- 1 | "#!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 09.04.2014 4 | 5 | ## Script performs the following tasks 6 | ## 1.) Retrieve a list of paused scans from a console. 7 | ## 2.) Retrieve a list of active scans from a console. 8 | ## 3.) Sort paused scans from least number of discovered assets to most 9 | ## 4.) Iteratively resume scans in batches for scans that have paused without completing. 10 | ## 5.) TODO: Massive code cleanup + efficiency improvements. 11 | 12 | ## Major code changes as of 11.21.2014 - BWG 13 | # Rearranged output information to a more logical order. 14 | # Screen output now includes additional information about the scan in the screen output. 15 | # Worked on Bug reduction. 16 | # - Fixed connection error information. 17 | # - Fixed issue with sessions being invalidating and never being recreated. 18 | # - Fixed yaml relative path issues with running the script from outside of its directory. 19 | # - Some code clean up. 20 | 21 | 22 | require 'yaml' 23 | require 'nexpose' 24 | 25 | include Nexpose 26 | 27 | # Default Values from yaml file 28 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 29 | config = YAML.load_file(config_path) 30 | 31 | @host = config["hostname"] 32 | @userid = config["username"] 33 | @password = config["passwordkey"] 34 | @port = config["port"] 35 | @consecutiveCleanupScans = config["cleanupqueue"] 36 | @cleanupWaitTime = config["cleanupwaittime"] 37 | @nexposeAjaxTimeout = config["nexposeajaxtimeout"] 38 | 39 | fillQueue = 0 40 | 41 | 42 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 43 | puts 'logging into Nexpose' 44 | 45 | begin 46 | nsc.login 47 | rescue ::Nexpose::APIError => err 48 | $stderr.puts("Connection failed: #{err.reason}") 49 | exit(1) 50 | end 51 | 52 | puts 'logged into Nexpose' 53 | at_exit { nsc.logout } 54 | 55 | 56 | ## Initialize connection timeout values. 57 | ## Timeout example provided by JGreen in https://community.rapid7.com/thread/5075 58 | 59 | module Nexpose 60 | class APIRequest 61 | include XMLUtils 62 | # Execute an API request 63 | def self.execute(url, req, api_version='2.0', options = {}) 64 | options = {timeout: @nexposeAjaxTimeout} 65 | obj = self.new(req.to_s, url, api_version) 66 | obj.execute(options) 67 | return obj 68 | end 69 | end 70 | 71 | 72 | module AJAX 73 | def self._https(nsc) 74 | http = Net::HTTP.new(nsc.host, nsc.port) 75 | http.use_ssl = true 76 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 77 | http.read_timeout = @nexposeAjaxTimeout 78 | http 79 | end 80 | end 81 | end 82 | 83 | 84 | ## Start loop that will continue until there are no longer any scans paused or actively running. 85 | begin 86 | begin 87 | puts "\r\nRequesting scan status updates from #{@host}\r\n" 88 | ## Pull data for paused scans - Method suggested by JGreen https://community.rapid7.com/thread/5075 (THANKS!!!) 89 | pausedScans = DataTable._get_dyn_table(nsc, '/data/site/scans/dyntable.xml?printDocType=0&tableID=siteScansTable&activeOnly=true').select { |scanHistory| (scanHistory['Status'].include? 'Paused')} 90 | ## Pull data for active scans 91 | activeScans = nsc.scan_activity() 92 | 93 | rescue Exception # should really list all the possible http exceptions 94 | puts "Connection issue detected - Retrying in #{@cleanupWaitTime} seconds)" 95 | sleep (@cleanupWaitTime) 96 | begin # This is a less than ideal bandaid to make sure there is a valid session. 97 | nsc.login 98 | rescue ::Nexpose::APIError => err 99 | $stderr.puts("Connection failed: #{err.reason}") 100 | exit(1) 101 | end 102 | retry 103 | end 104 | 105 | # Collect Site info to provide additional information for screen output. 106 | siteInfo = nsc.sites 107 | 108 | ## Attempting some basic prioritization to complete lower asset count scans first. 109 | ## Perform a destructive sort of the pausedScans array based on the number of discovered assets. 110 | pausedScans.sort! { |a,b| a['Devices Discovered'].to_i <=> b['Devices Discovered'].to_i } 111 | 112 | ## List all of the paused scans to stdout. 113 | puts "\r\n-- Paused Scans Detected : #{pausedScans.count} --\r\n" 114 | pausedScans.each do |scanHistory| 115 | siteInfoID = scanHistory['Site ID'].to_i 116 | begin 117 | puts "ScanID: #{scanHistory['Scan ID']}, Assets: #{scanHistory['Devices Discovered']}, SiteID: #{siteInfoID} - #{siteInfo[siteInfoID].name}, #{scanHistory['Status']}" 118 | rescue 119 | puts "ScanID: #{scanHistory['Scan ID']}, Assets: #{scanHistory['Devices Discovered']}, SiteID: #{siteInfoID}, #{scanHistory['Status']}" 120 | end 121 | end 122 | puts "-- Paused Scans Detected : #{pausedScans.count} --\r\n" 123 | 124 | 125 | puts "\r\n -- Queue Size: #{@consecutiveCleanupScans} -- \r\n" 126 | 127 | ## Output a list of active scans in the scan queue. 128 | activeScans.each do |status| 129 | siteInfoID = status.site_id 130 | begin 131 | puts "ScanID: #{status.scan_id}, Assets: #{status.nodes.live}, SiteID: #{status.site_id} - #{siteInfo[siteInfoID].name}, Status:#{status.status}, EngineID:#{status.engine_id}, StartTime:#{status.start_time}" 132 | rescue 133 | puts "ScanID: #{status.scan_id}, Assets: #{status.nodes.live}, SiteID: #{status.site_id}, Status:#{status.status}, EngineID:#{status.engine_id}, StartTime:#{status.start_time}" 134 | end 135 | end 136 | 137 | ## Check to see if there are any slots open in the cleanup queue and that there are still scans to resume. 138 | if ((activeScans.count < @consecutiveCleanupScans.to_i) and (pausedScans.count > 0)) 139 | 140 | ## Determine how many slots are left in the cleanup queue to use. 141 | fillQueue = ((@consecutiveCleanupScans - activeScans.count)-1) 142 | ## Loop through just enough paused scans to fill the open slots in the cleanup queue. 143 | pausedScans[0..fillQueue.to_i].each do |scanHistory| 144 | siteInfoID = scanHistory['Site ID'].to_i 145 | begin 146 | puts "Resuming ScanID: #{scanHistory['Scan ID']}, Assets: #{scanHistory['Devices Discovered']}, SiteID: #{siteInfoID} - #{siteInfo[siteInfoID].name}, Status: #{scanHistory['Status']}" 147 | rescue 148 | puts "Resuming ScanID: #{scanHistory['Scan ID']}, Assets: #{scanHistory['Devices Discovered']}, SiteID: #{siteInfoID}, Status: #{scanHistory['Status']}" 149 | end 150 | ## Resume the provided scanid. 151 | nsc.resume_scan(scanHistory['Scan ID']) 152 | end 153 | 154 | end 155 | 156 | if ((pausedScans.count + activeScans.count) > 0) 157 | ## Wait between checks so that the scans have time to run. 158 | sleep(@cleanupWaitTime) 159 | end 160 | 161 | ## If there are no more paused scans and the active scans have all completed without failing we can exit. 162 | end while ((pausedScans.count + activeScans.count) > 0) 163 | 164 | puts 'Logging out' 165 | exit -------------------------------------------------------------------------------- /scan_by_iplist.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 010.07.2016 4 | 5 | 6 | ## Script Heavily borrows from Steve Tempest : https://community.rapid7.com/docs/DOC-2733 , https://community.rapid7.com/docs/DOC-2732 7 | ## Written as PoC for https://community.rapid7.com/thread/9132 8 | 9 | ## Script performs the following tasks 10 | ## 1.) Read addresses from text file 11 | ## 2.) De-duplicate addresses 12 | ## 3.) Scan addresses 13 | ## 4.) Generate a report of vulnerabilities detected during the scan. 14 | 15 | 16 | require 'yaml' 17 | require 'nexpose' 18 | require 'csv' 19 | include Nexpose 20 | 21 | # Default Values 22 | 23 | config = YAML.load_file("conf/nexpose.yaml") # From file 24 | 25 | @host = config["hostname"] 26 | @userid = config["username"] 27 | @password = config["passwordkey"] 28 | @port = config["port"] 29 | @nexposeAjaxTimeout = config["nexposeajaxtimeout"] 30 | 31 | @name = nil 32 | 33 | # Any arguments after flags can be grabbed now." 34 | unless ARGV[0] 35 | $stderr.puts 'Input file and site id is required.' 36 | exit(1) 37 | end 38 | file = ARGV[0] 39 | @name = File.basename(file, File.extname(file)) unless @name 40 | 41 | siteID = 0 42 | siteID = ARGV[1] 43 | 44 | # This will fail if the file cannot be read. 45 | ips = File.read(file).split.uniq 46 | 47 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 48 | 49 | begin 50 | nsc.login 51 | rescue ::Nexpose::APIError => err 52 | $stderr.puts("Connection failed: #{err.reason}") 53 | exit(1) 54 | end 55 | 56 | at_exit { nsc.logout } 57 | 58 | ## Initialize connection timeout values. 59 | ## Timeout example provided by JGreen in https://community.rapid7.com/thread/5075 60 | 61 | module Nexpose 62 | class APIRequest 63 | include XMLUtils 64 | # Execute an API request 65 | def self.execute(url, req, api_version='2.0', options = {}) 66 | options = {timeout: @nexposeAjaxTimeout} 67 | obj = self.new(req.to_s, url, api_version) 68 | obj.execute(options) 69 | return obj 70 | end 71 | end 72 | 73 | 74 | module AJAX 75 | def self._https(nsc) 76 | http = Net::HTTP.new(nsc.host, nsc.port) 77 | http.use_ssl = true 78 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 79 | http.read_timeout = @nexposeAjaxTimeout 80 | http 81 | end 82 | end 83 | end 84 | 85 | 86 | # Create a map of all assets by IP to make them quicker to find. 87 | all_assets = nsc.assets.reduce({}) do |hash, dev| 88 | $stderr.puts("Duplicate asset: #{dev.address}") if @debug and hash.member? dev.address 89 | hash[dev.address] = dev 90 | hash 91 | end 92 | 93 | # Collect Site info to provide additional information for screen output. 94 | siteInfo = nsc.sites 95 | siteDetail = Site.load(nsc, siteID) 96 | @name = siteDetail.name 97 | 98 | puts "Starting partial scan of siteID #{siteID} " 99 | #scan = site.scan(nsc) 100 | 101 | scan = nsc.scan_ips(siteID, ips) 102 | 103 | 104 | begin 105 | sleep(15) 106 | status = nsc.scan_status(scan.id) 107 | puts "ScanID: #{scan.id} ScanTemplate: #{siteDetail.scan_template_id}, SiteID: #{siteID} - #{siteDetail.name}, Status:#{status}, EngineID:#{scan.engine}" 108 | end while status == Nexpose::Scan::Status::RUNNING 109 | 110 | 111 | sqlSelect = " 112 | WITH 113 | asset_ips AS ( 114 | SELECT asset_id, ip_address, type 115 | FROM dim_asset_ip_address dips 116 | ), 117 | asset_addresses AS ( 118 | SELECT da.asset_id, 119 | (SELECT array_to_string(array_agg(ip_address), ',') FROM asset_ips WHERE asset_id = da.asset_id AND type = 'IPv4') AS ipv4s, 120 | (SELECT array_to_string(array_agg(ip_address), ',') FROM asset_ips WHERE asset_id = da.asset_id AND type = 'IPv6') AS ipv6s, 121 | (SELECT array_to_string(array_agg(mac_address), ',') FROM dim_asset_mac_address WHERE asset_id = da.asset_id) AS macs 122 | FROM dim_asset da 123 | JOIN asset_ips USING (asset_id) 124 | ), 125 | asset_names AS ( 126 | SELECT asset_id, array_to_string(array_agg(host_name), ',') AS names 127 | FROM dim_asset_host_name 128 | GROUP BY asset_id 129 | ), 130 | asset_facts AS ( 131 | SELECT asset_id, riskscore, exploits, malware_kits 132 | FROM fact_asset 133 | ), 134 | vulnerability_metadata AS ( 135 | SELECT * 136 | FROM dim_vulnerability dv 137 | ), 138 | vuln_cves_ids AS ( 139 | SELECT vulnerability_id, array_to_string(array_agg(reference), ',') AS cves 140 | FROM dim_vulnerability_reference 141 | WHERE source = 'CVE' 142 | GROUP BY vulnerability_id 143 | ) 144 | 145 | 146 | SELECT 147 | da.ip_address AS \"Asset IP Address\", 148 | favi.port AS \"Service Port\", 149 | dp.name AS \"Service Protocol\", 150 | dsvc.name AS \"Service Name\", 151 | an.names AS \"Asset Names\", 152 | favi.date AS \"Vulnerability Test Date\", 153 | dsc.started AS \"Last Scan Time\", 154 | favi.scan_id AS \"Scan ID\", 155 | ds.name AS \"Site Name\", 156 | ds.importance AS \"Site Importance\", 157 | vm.date_published AS \"Vulnerability Published Date\", 158 | ROUND((EXTRACT(epoch FROM age(now(), date_published)) / (60 * 60 * 24))::numeric, 0) AS \"Vulnerability Age\", 159 | cves.cves AS \"Vulnerability CVE IDs\", 160 | vm.title AS \"Vulnerability Title\", 161 | vm.cvss_score AS \"Vulnerability CVSS Score\", 162 | proofAsText(vm.description) AS \"Vulnerability Description\", 163 | vm.nexpose_id AS \"Vulnerability ID\", 164 | vm.severity AS \"Vulnerability Severity Level\", 165 | dvs.description AS \"Vulnerability Test Result Description\" 166 | 167 | 168 | FROM fact_asset_vulnerability_instance favi 169 | JOIN dim_asset da USING (asset_id) 170 | LEFT OUTER JOIN asset_addresses aa USING (asset_id) 171 | LEFT OUTER JOIN asset_names an USING (asset_id) 172 | JOIN asset_facts af USING (asset_id) 173 | JOIN dim_service dsvc USING (service_id) 174 | JOIN dim_protocol dp USING (protocol_id) 175 | JOIN dim_site_asset dsa USING (asset_id) 176 | JOIN dim_site ds USING (site_id) 177 | JOIN vulnerability_metadata vm USING (vulnerability_id) 178 | JOIN dim_vulnerability_status dvs USING (status_id) 179 | JOIN dim_operating_system dos USING (operating_system_id) 180 | LEFT OUTER JOIN dim_scan dsc USING (scan_id) 181 | LEFT OUTER JOIN vuln_cves_ids cves USING (vulnerability_id) " 182 | 183 | sqlWhere = "" 184 | orderBy = " ORDER BY da.ip_address;" 185 | 186 | query = sqlSelect + sqlWhere + orderBy 187 | 188 | puts "Initiating Report" 189 | 190 | report = Nexpose::AdhocReportConfig.new(nil, 'sql') 191 | report.add_filter('version', '1.2.1') 192 | report.add_filter('query', query) 193 | report.add_filter('scan', scan.id) # filter the report scope based on the scanID that was just run. 194 | report_output = report.generate(nsc) 195 | csv_output = CSV.parse(report_output.chomp, { :headers => :first_row }) 196 | CSV.open("adhoc_SiteID_#{siteID}_Report.csv", 'w') do |csv_file| 197 | csv_file << csv_output.headers 198 | csv_output.each do |row| 199 | csv_file << row 200 | end 201 | end 202 | 203 | puts 'Report completed and saved' 204 | 205 | exit 206 | -------------------------------------------------------------------------------- /adhocScanGen.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 07.25.2014 4 | 5 | ## Script Heavily borrows from Steve Tempest : https://community.rapid7.com/docs/DOC-2733 , https://community.rapid7.com/docs/DOC-2732 6 | 7 | ## Script performs the following tasks 8 | ## 1.) Read addresses from text file 9 | ## 2.) De-duplicate addresses 10 | ## 3.) Create new temporary site 11 | ## 4.) Add addresses to the created site 12 | ## 5.) Specify scan engine and template to use from nexpose.yaml 13 | ## 6.) Perform scan of the temporary site. 14 | ## 7.) Generate a report of vulnerabilities detected 15 | ## 8.) Delete temporary site. 16 | 17 | require 'yaml' 18 | require 'nexpose' 19 | require 'optparse' 20 | require 'highline/import' 21 | require 'csv' 22 | 23 | 24 | # Default Values 25 | 26 | config = YAML.load_file("conf/nexpose.yaml") # From file 27 | 28 | @host = config["hostname"] 29 | @userid = config["username"] 30 | @password = config["passwordkey"] 31 | @port = config["port"] 32 | @scanTemplate = config["adhocscantemplate"] 33 | @scanEngine = config["adhocscanengine"] 34 | 35 | 36 | @name = @desc = nil 37 | 38 | OptionParser.new do |opts| 39 | opts.banner = "Usage: #{File::basename($0)} [options]" 40 | opts.separator '' 41 | opts.separator 'Create a temporary site based upon an input file, one address per line, scans, reports findings, then deletes the site.' 42 | opts.separator '' 43 | opts.separator 'By default, the filename is used as the name of the adhoc site, if --name is not provided.' 44 | opts.separator '' 45 | opts.separator 'Options:' 46 | opts.on('-n', '--name [NAME]', 'Name to use for the adhoc site. Must not already exist.') { |name| @name = name } 47 | opts.on('-d', '--desc [DESCRIPTION]', 'Description to use for new asset group.') { |desc| @desc = desc } 48 | opts.on('-x', '--debug', 'Report duplicate IP addresses to STDERR.') { |debug| @debug = debug } 49 | opts.on_tail('--help', 'Print this help message.') { puts opts; exit } 50 | end.parse! 51 | 52 | 53 | # Any arguments after flags can be grabbed now." 54 | unless ARGV[0] 55 | $stderr.puts 'Input file is required.' 56 | exit(1) 57 | end 58 | file = ARGV[0] 59 | @name = File.basename(file, File.extname(file)) unless @name 60 | 61 | # This will fail if the file cannot be read. 62 | ips = File.read(file).split.uniq 63 | 64 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 65 | puts 'logging into Nexpose' 66 | 67 | begin 68 | nsc.login 69 | rescue ::Nexpose::APIError => err 70 | $stderr.puts("Connection failed: #{e.reason}") 71 | exit(1) 72 | end 73 | 74 | puts 'logged into Nexpose' 75 | at_exit { nsc.logout } 76 | 77 | # Create a map of all assets by IP to make them quicker to find. 78 | all_assets = nsc.assets.reduce({}) do |hash, dev| 79 | $stderr.puts("Duplicate asset: #{dev.address}") if @debug and hash.member? dev.address 80 | hash[dev.address] = dev 81 | hash 82 | end 83 | 84 | puts "Creating site #{@name}" 85 | 86 | site = Nexpose::Site.new(@name, @scanTemplate) 87 | site.description = @desc 88 | site.engine = @scanEngine 89 | 90 | ips.each do |ip| 91 | site.add_asset(ip) 92 | puts "Adding #{ip} to site #{@name}" 93 | end 94 | 95 | site.save(nsc) 96 | 97 | puts 'Create site successfully' 98 | 99 | puts "Starting scan of adhoc site #{@name} " 100 | scan = site.scan(nsc) 101 | 102 | 103 | begin 104 | sleep(15) 105 | status = nsc.scan_status(scan.id) 106 | puts "Current scan status: #{status.to_s}" 107 | end while status == Nexpose::Scan::Status::RUNNING 108 | 109 | 110 | sqlSelect = " 111 | WITH 112 | asset_ips AS ( 113 | SELECT asset_id, ip_address, type 114 | FROM dim_asset_ip_address dips 115 | ), 116 | asset_addresses AS ( 117 | SELECT da.asset_id, 118 | (SELECT array_to_string(array_agg(ip_address), ',') FROM asset_ips WHERE asset_id = da.asset_id AND type = 'IPv4') AS ipv4s, 119 | (SELECT array_to_string(array_agg(ip_address), ',') FROM asset_ips WHERE asset_id = da.asset_id AND type = 'IPv6') AS ipv6s, 120 | (SELECT array_to_string(array_agg(mac_address), ',') FROM dim_asset_mac_address WHERE asset_id = da.asset_id) AS macs 121 | FROM dim_asset da 122 | JOIN asset_ips USING (asset_id) 123 | ), 124 | asset_names AS ( 125 | SELECT asset_id, array_to_string(array_agg(host_name), ',') AS names 126 | FROM dim_asset_host_name 127 | GROUP BY asset_id 128 | ), 129 | asset_facts AS ( 130 | SELECT asset_id, riskscore, exploits, malware_kits 131 | FROM fact_asset 132 | ), 133 | vulnerability_metadata AS ( 134 | SELECT * 135 | FROM dim_vulnerability dv 136 | ), 137 | vuln_cves_ids AS ( 138 | SELECT vulnerability_id, array_to_string(array_agg(reference), ',') AS cves 139 | FROM dim_vulnerability_reference 140 | WHERE source = 'CVE' 141 | GROUP BY vulnerability_id 142 | ) 143 | 144 | 145 | SELECT 146 | da.ip_address AS \"Asset IP Address\", 147 | favi.port AS \"Service Port\", 148 | dp.name AS \"Service Protocol\", 149 | dsvc.name AS \"Service Name\", 150 | an.names AS \"Asset Names\", 151 | favi.date AS \"Vulnerability Test Date\", 152 | dsc.started AS \"Last Scan Time\", 153 | favi.scan_id AS \"Scan ID\", 154 | ds.name AS \"Site Name\", 155 | ds.importance AS \"Site Importance\", 156 | vm.date_published AS \"Vulnerability Published Date\", 157 | ROUND((EXTRACT(epoch FROM age(now(), date_published)) / (60 * 60 * 24))::numeric, 0) AS \"Vulnerability Age\", 158 | cves.cves AS \"Vulnerability CVE IDs\", 159 | vm.title AS \"Vulnerability Title\", 160 | vm.cvss_score AS \"Vulnerability CVSS Score\", 161 | proofAsText(vm.description) AS \"Vulnerability Description\", 162 | vm.nexpose_id AS \"Vulnerability ID\", 163 | vm.severity AS \"Vulnerability Severity Level\", 164 | dvs.description AS \"Vulnerability Test Result Description\" 165 | 166 | 167 | FROM fact_asset_vulnerability_instance favi 168 | JOIN dim_asset da USING (asset_id) 169 | LEFT OUTER JOIN asset_addresses aa USING (asset_id) 170 | LEFT OUTER JOIN asset_names an USING (asset_id) 171 | JOIN asset_facts af USING (asset_id) 172 | JOIN dim_service dsvc USING (service_id) 173 | JOIN dim_protocol dp USING (protocol_id) 174 | JOIN dim_site_asset dsa USING (asset_id) 175 | JOIN dim_site ds USING (site_id) 176 | JOIN vulnerability_metadata vm USING (vulnerability_id) 177 | JOIN dim_vulnerability_status dvs USING (status_id) 178 | JOIN dim_operating_system dos USING (operating_system_id) 179 | LEFT OUTER JOIN dim_scan dsc USING (scan_id) 180 | LEFT OUTER JOIN vuln_cves_ids cves USING (vulnerability_id) " 181 | 182 | sqlWhere = "WHERE ds.name LIKE '#{@name}';" 183 | 184 | query = sqlSelect + sqlWhere 185 | 186 | 187 | report = Nexpose::AdhocReportConfig.new(nil, 'sql') 188 | report.add_filter('version', '1.2.1') 189 | report.add_filter('query', query) 190 | report.add_filter('group', 1) 191 | report_output = report.generate(nsc) 192 | csv_output = CSV.parse(report_output.chomp, { :headers => :first_row }) 193 | CSV.open("adhoc#{@name}Report.csv", 'w') do |csv_file| 194 | csv_file << csv_output.headers 195 | csv_output.each do |row| 196 | csv_file << row 197 | end 198 | end 199 | 200 | puts 'Report completed and saved, deleting site' 201 | site.delete(nsc) 202 | 203 | puts 'Site deleted, logging out' 204 | exit 205 | -------------------------------------------------------------------------------- /smartCleanup.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # 01.22.2015 4 | 5 | ## Script performs the following tasks 6 | ## 1.) Retrieve a list of paused scans from a console. 7 | ## 2.) Retrieve a list of active scans from a console. 8 | ## 3.) Sort paused scans from least number of discovered assets to most 9 | ## 4.) Iteratively resume scans in batches for scans that have paused without completing. 10 | ## 5.) 11 | ## 6.) TODO: Massive code cleanup + efficiency improvements. 12 | 13 | ## Major code changes as of 11.21.2014 - BWG 14 | # Rearranged output information to a more logical order. 15 | # Screen output now includes additional information about the scan in the screen output. 16 | # Worked on Bug reduction. 17 | # - Fixed connection error information. 18 | # - Fixed issue with sessions being invalidating and never being recreated. 19 | # - Fixed yaml relative path issues with running the script from outside of its directory. 20 | # - Some code clean up. 21 | 22 | ## Update - 02.23.2016 - BWG 23 | # Changed how paused scans are pulled basd on https://community.rapid7.com/thread/7904 24 | ## Update - 04.25.2017 - BWG 25 | # Code refactor 26 | 27 | #require 'pp' 28 | require 'yaml' 29 | require 'nexpose' 30 | require 'time' 31 | include Nexpose 32 | 33 | # Default Values from yaml file 34 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 35 | config = YAML.load_file(config_path) 36 | 37 | @host = config["hostname"] 38 | @userid = config["username"] 39 | @password = config["passwordkey"] 40 | @port = config["port"] 41 | @nexposeAjaxTimeout = config["nexposeajaxtimeout"] 42 | 43 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 44 | 45 | begin 46 | nsc.login 47 | rescue ::Nexpose::APIError => err 48 | $stderr.puts("Connection failed: #{err.reason}") 49 | exit(1) 50 | raise 51 | end 52 | at_exit { nsc.logout } 53 | 54 | =begin 55 | ## Initialize connection timeout values. 56 | module Nexpose 57 | class APIRequest 58 | include XMLUtils 59 | # Execute an API request (5th param used for gem version 5.3.0+) 60 | def self.execute(url, req, api_version='2.0', options = {}, trust_store = nil) 61 | options = {timeout: 6000000} 62 | obj = self.new(req.to_s, url, api_version, trust_store) 63 | obj.execute(options) 64 | return obj 65 | end 66 | end 67 | 68 | module AJAX 69 | def self.https(nsc, timeout = nil) 70 | http = Net::HTTP.new(nsc.host, nsc.port) 71 | http.use_ssl = true 72 | # changes for gem version 5.3.0+ 73 | if nsc.trust_store.nil? 74 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 75 | else 76 | http.cert_store = nsc.trust_store 77 | end 78 | http.read_timeout = @nexposeAjaxTimeout 79 | http.open_timeout = @nexposeAjaxTimeout 80 | http.continue_timeout = @nexposeAjaxTimeout 81 | http 82 | end 83 | end 84 | end 85 | =end 86 | 87 | def scan_status(nsc, config) 88 | begin 89 | puts "\r\nRequesting scan status updates from #{@host}\r\n" 90 | ## Pull data for paused scans 91 | pausedScans = nsc.paused_scans 92 | ## Pull data for active scans 93 | activeScans = nsc.scan_activity() 94 | rescue Exception # should really list all the possible http exceptions 95 | puts "Connection issue detected - Retrying in #{config["cleanupwaittime"]} seconds)" 96 | sleep (config["cleanupwaittime"]) 97 | begin # This is a less than ideal bandaid to make sure there is a valid session. 98 | nsc.login 99 | rescue ::Nexpose::APIError => err 100 | $stderr.puts("Connection failed: #{err.reason}") 101 | exit(1) 102 | raise 103 | end 104 | retry 105 | end 106 | scans = {:activeScans => activeScans, :pausedScans => pausedScans} 107 | return scans 108 | end 109 | 110 | def list_paused_scans(nsc, config, scans) 111 | ## Attempting some basic prioritization to complete lower asset count scans first. 112 | ## Perform a destructive sort of the pausedScans array based on the number of discovered assets. 113 | scans[:pausedScans].sort! { |a,b| a.assets.to_i <=> b.assets.to_i } 114 | # Collect Site info to provide additional information for screen output. 115 | siteInfo = nsc.sites 116 | ## List all of the paused scans to stdout. 117 | puts "\r\n-- Paused Scans Detected : #{scans[:pausedScans].count} --\r\n" 118 | scans[:pausedScans].each do |scanHistory| 119 | siteInfoID = scanHistory.site_id.to_i 120 | siteDetail = Site.load(nsc, siteInfoID) 121 | ## scanDetail = ScanSummary.select{ |scanInfo| (scanInfo['scan_id'].include? scanHistory.id.to_i)} 122 | begin 123 | puts "ScanID: #{scanHistory.id}, Assets: #{scanHistory.assets}, ScanTemplate: #{siteDetail.scan_template_id}, SiteID: #{siteInfoID} - #{siteDetail.name}, #{scanHistory.status}" 124 | rescue 125 | #raise 126 | end 127 | end 128 | puts "\r\n\r\n" 129 | end 130 | 131 | def list_active_scans(nsc, config, scans) 132 | hostCount = 0 133 | puts "-- Active Scans Detected : #{scans[:activeScans].count} | Queue Size: #{config["cleanupqueue"]}. --\r\n" 134 | ## Output a list of active scans in the scan queue. 135 | scans[:activeScans].each do |status| 136 | siteInfoID = status.site_id 137 | siteDetail = Site.load(nsc, siteInfoID) 138 | ## scanDetail = ScanSummary.select{ |scanInfo| (scanInfo['scan_id'].include? scanHistory['Scan ID'].to_i)} 139 | begin 140 | Scan 141 | puts "ScanID: #{status.scan_id}, Assets: #{status.nodes.live}, ScanTemplate: #{siteDetail.scan_template_id}, SiteID: #{status.site_id} - #{siteDetail.name}, Status:#{status.status}, EngineID:#{status.engine_id}, StartTime:#{status.start_time}" 142 | hostCount += status.nodes.live 143 | rescue 144 | #raise 145 | end 146 | end 147 | return hostCount 148 | end 149 | 150 | def resume_scans(nsc, config, scans) 151 | fillQueue = hostCount = 0 152 | ## Check to see if there are any slots open in the cleanup queue and that there are still scans to resume. 153 | if ((scans[:activeScans].count < config["cleanupqueue"]) and (scans[:pausedScans].count > 0)) 154 | ## Determine how many slots are left in the cleanup queue to use. 155 | fillQueue = ((config["cleanupqueue"] - scans[:activeScans].count) -1) 156 | ## Loop through just enough paused scans to fill the open slots in the cleanup queue. 157 | scans[:pausedScans][0..fillQueue.to_i].each do |scanHistory| 158 | siteInfoID = scanHistory.site_id.to_i 159 | siteDetail = Site.load(nsc, siteInfoID) 160 | begin 161 | hostCount += scanHistory.assets.to_i # Count the number of hosts being scanned. 162 | puts "Resuming ScanID: #{scanHistory.id}, Assets: #{scanHistory.assets}, ScanTemplate: #{siteDetail.scan_template_id}, SiteID: #{siteInfoID} - #{siteDetail.name}, Status: #{scanHistory.status}" 163 | rescue 164 | raise 165 | end 166 | 167 | begin 168 | # Resume the provided scanid. 169 | nsc.resume_scan(scanHistory.id) 170 | rescue 171 | end 172 | end 173 | end 174 | return hostCount 175 | end 176 | 177 | ## Start loop that will continue until there are no longer any scans paused or actively running. 178 | begin 179 | scans = scan_status(nsc, config) 180 | list_paused_scans(nsc, config, scans) 181 | hostCount = 0 # Initialize hostCount. 182 | hostCount += list_active_scans(nsc, config, scans) 183 | hostCount += resume_scans(nsc, config, scans) 184 | puts "Total expected Active hosts being scanned: #{hostCount} - #{Time.now}\r\n\r\nNext Run time: #{Time.now + config["cleanupwaittime"]}\r\n\r\n" 185 | if ((scans[:pausedScans].count + scans[:activeScans].count) > 0) 186 | ## Wait between checks so that the scans have time to run. 187 | sleep(config["cleanupwaittime"]) 188 | end 189 | ## If there are no more paused scans and the active scans have all completed without failing we can exit. 190 | end while ((scans[:pausedScans].count + scans[:activeScans].count) > 0) 191 | 192 | puts 'Logging out' 193 | exit 194 | -------------------------------------------------------------------------------- /systemCheck.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Brian W. Gray 4 | # 06.08.2015 5 | # 6 | # Purpose: Perform the health checks and perform corrective actions when able. 7 | # 1.) Pull system info 8 | # 2.) Pull List of available Backups 9 | # 3.) Check Console Name 10 | # 4.) Check OS 11 | # 5.) Check Console Version 12 | # 6.) Check up time 13 | # 7.) Check console memory utilization 14 | # 8.) Check console CPU information 15 | # 9.) Check DB Version 16 | # 10.) Check Java Information 17 | # 11.) List available scan engines 18 | # 12.) List available scan pools 19 | # 13.) List scan pool member engines 20 | # 14.) Update status 21 | # 15.) Console performance monitor and limited issue resolution. 22 | # 23 | # Original idea and the start of much of the code sourced from https://github.com/dc401/NexposeRubyScripts/blob/master/prescan_healthcheck.rb 24 | 25 | require 'yaml' 26 | require 'rubygems' 27 | require 'nexpose' 28 | # require 'Time' 29 | require 'io/console' 30 | require 'pp' 31 | 32 | include Nexpose 33 | 34 | # Default Values from yaml file 35 | config_path = File.expand_path("../conf/nexpose.yaml", __FILE__) 36 | config = YAML.load_file(config_path) 37 | 38 | @host = config["hostname"] 39 | @userid = config["username"] 40 | @password = config["passwordkey"] 41 | @port = config["port"] 42 | @serviceTimeout = config["servicetimeout"] 43 | 44 | # Acceptable uptime value. (uptime at least this value) 45 | nscUpTimeThreshold = 600 46 | 47 | 48 | #One blink for yes, two blinks for no! 49 | class Beep 50 | #The use of "self" init a class method rather than instance method 51 | def self.pass 52 | print "\a" 53 | end 54 | 55 | def self.fail 56 | print "\a \a" 57 | end 58 | end 59 | 60 | def checkService() 61 | tryAgain = 0 62 | 63 | begin 64 | begin 65 | path = '/login.html' # Check to see if we may login or if we are re-directed to the maintenance login page. 66 | 67 | http = Net::HTTP.new(@host,@port) 68 | http.read_timeout = 1 69 | http.use_ssl = true 70 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 71 | response = nil 72 | 73 | http.start{|http| 74 | request = Net::HTTP::Get.new(path) 75 | response = http.request(request) 76 | } 77 | 78 | rescue Exception # should really list all the possible http exceptions 79 | puts "Attempt: #{tryAgain} Service Unavailable" 80 | sleep (30) 81 | retry if (tryAgain += 1) < @serviceTimeout 82 | end 83 | 84 | response.code 85 | if response.code == "200" # Check the status code anything other than 200 indicates the service is not ready. 86 | puts "Attempt: #{tryAgain} #{response.code} The Nexpose Service appears to be up and functional" 87 | tryAgain = @serviceTimeout 88 | else 89 | puts "Attempt: #{tryAgain} #{response.code} #{response.message} The Service is not yet fully initialized" 90 | tryAgain += 1 91 | sleep(30) 92 | end 93 | end while tryAgain < @serviceTimeout 94 | 95 | if (response.code != "200") 96 | puts "The service was never determined to be available. Action Timed Out" 97 | exit 98 | end 99 | end 100 | 101 | 102 | 103 | # 104 | # Connect and authenticate 105 | # 106 | nsc = Nexpose::Connection.new(@host, @userid, @password, @port) 107 | 108 | begin 109 | checkService() 110 | nsc.login 111 | rescue ::Nexpose::APIError => err 112 | $stderr.puts("Connection failed: #{err.reason}") 113 | exit(1) 114 | end 115 | 116 | at_exit { nsc.logout } 117 | 118 | #Pull system info 119 | sysInfo = nsc.system_information 120 | 121 | #Pull List of available Backups 122 | listBackups = nsc.list_backups 123 | 124 | 125 | # temp read all available info 126 | # pp sysInfo 127 | 128 | 129 | ## Gather console information ## 130 | 131 | #Check Console Name 132 | nscName = sysInfo["nsc-name"] 133 | 134 | #Check OS 135 | nscOs = sysInfo["os"] 136 | 137 | #Check Console Version 138 | nscConsoleVersion = sysInfo["nsc-version"] 139 | nscConsoleUpdateId = sysInfo["last-update-id"] 140 | nscLastUpdate = sysInfo["last-update-date"] 141 | nscVersion = sysInfo["nsc-version"] 142 | 143 | #Check up time 144 | nscUpTime = sysInfo["up-time"] 145 | 146 | #Check memory utilization 147 | nscFreeMem = sysInfo["ram-free"] 148 | nscTotalMem = sysInfo["ram-total"] 149 | 150 | #Check CPU information 151 | nscCpuCount = sysInfo["cpu-count"] 152 | nscCpuSpeed = sysInfo["cpu-speed"] 153 | 154 | #Check DB Version 155 | nscDbProduct = sysInfo["db-product"] 156 | nscDbVersion = sysInfo["db-version"] 157 | 158 | #Check Java Information 159 | nscJavaName = sysInfo["java-name"] 160 | nscJavaHeapMax = sysInfo["java-heap-max"] 161 | nscJavaHeapCommitted = sysInfo["java-heap-committed"] 162 | nscJavaHeapFree = sysInfo["java-heap-free"] 163 | nscJavaHeapUsed = sysInfo["java-heap-used"] 164 | nscJreVersion = sysInfo["jre-version"] 165 | nscJavaDaemonThreadCount = sysInfo["java-daemon-thread-count"] 166 | nscJavaTotalThreadCount = sysInfo["java-total-thread-count"] 167 | nscJavaThreadPeakCount = sysInfo["java-thread-peak-count"] 168 | nscJavaStartedThreadCount = sysInfo["java-started-thread-count"] 169 | 170 | 171 | 172 | ## Output Collected Data ## 173 | puts #Blank line 174 | puts "---- Console Information ----" 175 | puts #Blank line 176 | 177 | #Check Console Name 178 | puts "Console Name: #{nscName}" 179 | 180 | #Check OS 181 | puts "Console Operating System: #{nscOs}" 182 | 183 | #Check Console Version 184 | puts "Console Version: #{nscConsoleVersion}" 185 | puts "Console Update ID: #{nscConsoleUpdateId}" 186 | puts "Console Last Update: #{nscLastUpdate}" 187 | puts "Console Version: #{nscVersion}" 188 | 189 | #Check up time 190 | puts "Console Uptime: #{nscUpTime}" 191 | 192 | #Check memory utilization 193 | puts "Console Free Memory: #{nscFreeMem}" 194 | puts "Console Total Memory: #{nscTotalMem}" 195 | 196 | #Check CPU information 197 | puts "Number of Console CPUs: #{nscCpuCount}" 198 | puts "Speed of Console Processors: #{nscCpuSpeed}" 199 | 200 | #Check DB Version 201 | puts "Console Database Type: #{nscDbProduct}" 202 | puts "Console Database Version: #{nscDbVersion}" 203 | 204 | #Check Java Information 205 | puts "Console Java Information" 206 | puts "Name: #{nscJavaName}" 207 | puts "Heap Max: #{nscJavaHeapMax}" 208 | puts "Heap Committed: #{nscJavaHeapCommitted}" 209 | puts "Heap Free: #{nscJavaHeapFree}" 210 | puts "Heap Used: #{nscJavaHeapUsed}" 211 | puts "JRE Version: #{nscJreVersion}" 212 | puts "Daemon Thread Count: #{nscJavaDaemonThreadCount}" 213 | puts "Total Thread Count: #{nscJavaTotalThreadCount}" 214 | puts "Peak Thread Count: #{nscJavaThreadPeakCount}" 215 | puts "Started Thread Count: #{nscJavaStartedThreadCount}" 216 | 217 | 218 | puts #Blank line 219 | puts "---- List all configured console users ----" 220 | puts #Blank line 221 | 222 | nsc.list_users.each do |listUsers| 223 | puts "User Name: #{listUsers.name}" 224 | puts "Full Name: #{listUsers.full_name}" 225 | puts "Email Address: #{listUsers.email}" 226 | puts "Admin User? #{listUsers.is_admin}" 227 | puts "Disabled? #{listUsers.is_disabled}" 228 | puts "Locked? #{listUsers.is_locked}" 229 | puts "Auth Source: #{listUsers.auth_source}" 230 | # puts "#{listUsers.}" 231 | puts # Blank line 232 | end 233 | 234 | 235 | #Pull the list of available backups 236 | if listBackups.any? 237 | 238 | puts #Blank line 239 | puts "---- List of available Backups on #{@host} ----" 240 | puts #Blank line 241 | 242 | listBackups.each do |backupList| 243 | puts "Name: #{backupList.name} Description: #{backupList.description} size: #{backupList.size} Date : #{backupList.date}" 244 | end 245 | 246 | end 247 | 248 | 249 | puts #Blank line 250 | puts "---- List of Available Scan Engines ----" 251 | puts #Blank line 252 | 253 | # Pull scan engine status / ensure engine status is current. 254 | ## versionEngines = nsc.console_command('version engines') 255 | 256 | ## I don't use a rapid7 hosted scan engine so I've excluded it due to timeouts etc. attempting to refresh it. 257 | 258 | # Pull Engine version information to for reference below 259 | engineVer = nsc.engine_versions 260 | 261 | engine_ids = Array.new 262 | nsc.engines.each do |engine| 263 | unless engine.name.include?("Rapid7 Hosted Scan Engine") 264 | engine_ids << engine.id 265 | end 266 | end 267 | 268 | # Disabled until I track down the new api request location 269 | # Nexpose::AJAX.post(nsc, "/ajax/engine-refreshAll.txml", "engineIds=#{engine_ids * ','}", Nexpose::AJAX::CONTENT_TYPE::FORM) 270 | 271 | engine_list = {} 272 | nsc.engines.each do |engine| 273 | engine_list[engine.id] = "#{engine.name} (#{engine.status})" 274 | puts "Engine Name: #{engine.name}" 275 | puts " Engine ID: #{engine.id}" 276 | puts " Scope: #{engine.scope}" 277 | puts " Address: #{engine.address}" 278 | puts " Port: #{engine.port}" 279 | puts " Status: #{engine.status}" 280 | puts #Blank line 281 | 282 | # puts versionEngines 283 | engineVer.each do |enVerInfo| 284 | if enVerInfo["Name"].include?(engine.name) 285 | puts " DN: #{enVerInfo["DN"]}" 286 | puts " Version: #{enVerInfo["Version"]}" 287 | puts " Address (FQDN): #{enVerInfo["Address (FQDN)"]}" 288 | puts " Platform: #{enVerInfo["Platform"]}" 289 | puts " Serial No: #{enVerInfo["Serial No"]}" 290 | puts " Product Name: #{enVerInfo["Product Name"]}" 291 | puts " Last Content Update ID: #{enVerInfo["Last Content Update ID"]}" 292 | puts " Last Auto Content Update ID: #{enVerInfo["Last Auto Content Update ID"]}" 293 | puts " Last Product Update ID: #{enVerInfo["Last Product Update ID"]}" 294 | puts " Software Revision: #{enVerInfo["Software Revision"]}" 295 | puts " Product ID: #{enVerInfo["Product ID"]}" 296 | puts " Version ID: #{enVerInfo["Version ID"]}" 297 | puts " VM Version: #{enVerInfo["VM Version"]}" 298 | puts 299 | end 300 | end 301 | end 302 | 303 | 304 | engineVer = nsc.engine_versions 305 | 306 | puts #Blank line 307 | puts "---- List of Available Engine Pools ----" 308 | puts #Blank line 309 | 310 | 311 | 312 | nsc.engine_pools.each do |enginePool| 313 | puts "Pool Name: #{enginePool.name}" 314 | puts " Pool ID: #{enginePool.id}" 315 | puts " Pool Scope: #{enginePool.scope}" 316 | puts #Blank line 317 | puts " --- Engines ---" 318 | 319 | if !enginePool.name.include?('Default Engine Pool') 320 | poolConf = Nexpose::EnginePool::load(nsc, enginePool.name, 'silo') 321 | 322 | poolConf.engines.each do |poolEng| 323 | 324 | puts " Engine ID: #{poolEng.id}" 325 | puts " Engine Name: #{poolEng.name}" 326 | puts " Engine Address: #{poolEng.address}" 327 | puts " Engine Port: #{poolEng.port}" 328 | puts " Engine Scope: #{poolEng.scope}" 329 | puts " Engine Status: #{poolEng.status}" 330 | puts 331 | 332 | end 333 | end 334 | end 335 | 336 | 337 | 338 | puts #Blank line 339 | puts "---- Diagnostics ----" 340 | puts #Blank line 341 | 342 | puts "== Update status ==" 343 | puts # blank line 344 | 345 | begin 346 | #Check for the last update 347 | CTime = Time.now.to_i 348 | #Check to see if the update was within last 7 days 349 | if (CTime + 604800) < nscLastUpdate.to_i 350 | puts "Last Update: OK" 351 | elsif (CTime + 604800) >= nscLastUpdate.to_i 352 | puts "Last Update: Not Updated within 7 days" 353 | puts Time.at(CTime) 354 | Beep.fail 355 | puts "Starting update. Please wait." 356 | nscUpdate = nsc.console_command("updatenow") 357 | puts nscUpdate 358 | puts "Pushing update to scan engines. Please wait." 359 | engineUpdate = nsc.console_command("update engines") # throws error after api changes made to the application 360 | end 361 | 362 | rescue StandardError => err 363 | print err 364 | 365 | end 366 | 367 | puts # blank line 368 | puts "== Console Performance ==" 369 | puts # blank line 370 | 371 | begin 372 | 373 | #Check to see if up time is greater than 5 minutes 374 | if nscUpTime.to_i >= nscUpTimeThreshold 375 | puts "Uptime: #{nscUpTime} - OK " 376 | elsif nscUpTime.to_i <= nscUpTimeThreshold 377 | puts "Uptime: #{nscUpTime} - Recent service restart. Potential Issue." 378 | Beep.fail 379 | end 380 | 381 | rescue StandardError => err 382 | print err 383 | 384 | end 385 | 386 | 387 | begin 388 | nscMemUse = (nscFreeMem.to_i / nscTotalMem.to_i) 389 | #Check to see if we are at 75% or greater usage 390 | if nscMemUse < (0.75) 391 | puts "Memory Usage: OK #{(nscMemUse)}%" 392 | elsif nscMemUse >= (0.75) 393 | puts "Memory Usage: Above 75%" 394 | puts "Utilization: #{(nscMemUse.to_i * 10)}%" 395 | Beep.fail 396 | puts "Attempting to free up Java resources. Please wait." 397 | GarbageCollect = nsc.console_command("garbagecollect") 398 | puts GarbageCollect 399 | end 400 | 401 | rescue StandardError => err 402 | print err 403 | 404 | end 405 | 406 | 407 | 408 | puts #Blank line 409 | puts "---- End Diagnostics ----" 410 | puts #Blank line 411 | 412 | exit 413 | -------------------------------------------------------------------------------- /vulnReporter.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Brian W. Gray 3 | # Initial code generated: 10.11.2016 4 | 5 | # Purpose of this script. 6 | # Query for notifiable vulnerabilities and initiate configured actions. 7 | 8 | # This script queries all scans that occured in a specified time range from a console and then takes action 9 | # on systems that have been found to be vulnerable. 10 | 11 | # Dependencies required to be installed: 12 | # sudo gem install nexpose 13 | # sudo gem install yaml 14 | # for an example ./conf/nexpose.yaml see https://github.com/BrianWGray/nexpose/blob/master/conf/nexpose.yaml 15 | 16 | require 'yaml' # used to parse configuration files 17 | require 'nexpose' # makes the world turn 18 | require 'time' # supports timestamping and time manipulation for queries 19 | require 'active_support/all' # used for time rounding methods 20 | require 'htmlentities' # used for html entity filters 21 | require 'json' # used for json support 22 | require 'csv' # enables csv parsing of reports to hashes 23 | require 'net/smtp' # used to support proof of concept email notices 24 | require 'pp' # lazy trouble shooting 25 | 26 | # Default Values from yaml file 27 | configPath = File.expand_path("../conf/pgh-nvs-01.yaml", __FILE__) 28 | config = YAML.load_file(configPath) 29 | vulNotifyPath = File.expand_path("../conf/vulnotify.yaml", __FILE__) 30 | vulNotify = YAML.load_file(vulNotifyPath) 31 | 32 | # debug sets verbose output to stdout. 33 | debug = config["vrDebug"] 34 | 35 | host = config["hostname"] 36 | userid = config["username"] 37 | password = config["passwordkey"] 38 | port = config["port"] 39 | @nexposeAjaxTimeout = config["nexposeajaxtimeout"] 40 | 41 | ageInterval = config["ageInterval"] # => Integer In Hours defines the number of hours to subtract from the specified query time to create a query time window. 42 | 43 | # If you need to add auth: https://www.tutorialspoint.com/ruby/ruby_sending_email.htm 44 | mailFrom = config["mailFrom"] 45 | mailServer = config["mailServer"] 46 | mailPort = config["mailPort"] 47 | mailDomain = config["mailDomain"] 48 | 49 | # Default Email address for notifications. 50 | defaultEmail = config["defaultEmail"] 51 | mailTo = defaultEmail 52 | 53 | # Number of threads alotted for consecutive nexpose_id's queried 54 | threadLimit = config["vulnReporterThreads"] 55 | 56 | ## Initialize connection timeout values. 57 | ## Timeout example provided by JGreen in https://community.rapid7.com/thread/5075 58 | # Here we extend the default web request timeouts for the script 59 | module Nexpose 60 | class APIRequest 61 | include XMLUtils 62 | # Execute an API request 63 | def self.execute(url, req, api_version='1.2', options = {}) 64 | options = {timeout: @nexposeAjaxTimeout} 65 | obj = self.new(req.to_s, url, api_version) 66 | obj.execute(options) 67 | return obj 68 | end 69 | end 70 | 71 | module AJAX 72 | def self._https(nsc) 73 | http = Net::HTTP.new(nsc.host, nsc.port) 74 | http.use_ssl = true 75 | # http.verify_mode = OpenSSL::SSL::VERIFY_NONE 76 | http.read_timeout = @nexposeAjaxTimeout 77 | http 78 | end 79 | end 80 | end 81 | 82 | # Support for changing blank csv fields to nil during csv parsing 83 | CSV::Converters[:blank_to_nil] = lambda do |field| 84 | field && field.empty? ? nil : field 85 | end 86 | 87 | # Display generic debug info to stdout 88 | def debug_print(returnedData, debug="false") 89 | if debug == "true" then 90 | puts "\r\n[DEBUG]\r\n" 91 | pp(returnedData) 92 | end 93 | end 94 | 95 | def checkService(config) 96 | tryAgain = 0 97 | 98 | host = config["hostname"] 99 | userid = config["username"] 100 | password = config["passwordkey"] 101 | port = config["port"] 102 | @nexposeAjaxTimeout = config["nexposeajaxtimeout"] 103 | @serviceTimeout = config["servicetimeout"] 104 | 105 | begin 106 | begin 107 | path = '/login.html' # Check to see if we may login or if we are re-directed to the maintenance login page. 108 | 109 | http = Net::HTTP.new(host,port) 110 | http.read_timeout = 1 111 | http.use_ssl = true 112 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 113 | response = nil 114 | 115 | http.start{|http| 116 | request = Net::HTTP::Get.new(path) 117 | response = http.request(request) 118 | } 119 | 120 | rescue Exception # should really list all the possible http exceptions 121 | puts "Attempt: #{tryAgain} Service Unavailable" 122 | sleep (30) 123 | retry if (tryAgain += 1) < @serviceTimeout 124 | end 125 | 126 | # response.code 127 | if response.code == "200" # Check the status code anything other than 200 indicates the service is not ready. 128 | puts "Attempt: #{tryAgain} #{response.code} The Nexpose Service appears to be up and functional" 129 | tryAgain = @serviceTimeout 130 | else 131 | puts "Attempt: #{tryAgain} #{response.code} #{response.message} The Service is not yet fully initialized" 132 | tryAgain += 1 133 | sleep(30) 134 | end 135 | end while tryAgain < @serviceTimeout 136 | 137 | if (response.code != "200") 138 | puts "The service was never determined to be available. Action Timed Out" 139 | exit 140 | end 141 | end 142 | 143 | # Take time in various formats and normalize it to a time object 144 | def normalize_time(time, debug) 145 | begin 146 | time = time if(time.is_a?(Time)) 147 | time = Time.parse("#{time.to_s}") if(!time.is_a?(Time)) 148 | rescue 149 | time = Time.now # Upon failure use the current time value 150 | end 151 | 152 | return time 153 | end 154 | 155 | # Record the current time when the query client is run 156 | def query_time(lastRunFile, time=nil, debug) 157 | 158 | # TODO: 159 | # write the current run time to the lastRunFile location 160 | currentRunTime = normalize_time(time, debug) 161 | 162 | lastRunTime = normalize_time(time, debug) 163 | 164 | # Running the query hourly we start the query at the beginning of the hour... (subject to change) 165 | return lastRunTime.beginning_of_hour() 166 | end 167 | 168 | # Determine the last time the reporting client ran a query 169 | def last_query_time(lastRunFile, ageInterval, time=nil, debug) 170 | 171 | # TODO: 172 | # Was the run successful? 173 | loggedRunTime = nil 174 | # Read last run file and pull the last date entry in the file to determine how far back to query for new vulnerabilities 175 | 176 | 177 | # If there is not previous logged run time assume the default time scope 178 | loggedRunTime ? lastRunTime = normalize_time(loggedRunTime, debug) : lastRunTime = (normalize_time(time, debug) - ageInterval.hours).to_datetime 179 | 180 | return lastRunTime 181 | end 182 | 183 | 184 | # Query a defined nexpose console for all vulnerabilities matching the listed vulnId value 185 | def query_vulns(nexposeId, nsc, debug) 186 | 187 | @sqlSelect = "SELECT * FROM dim_vulnerability " 188 | @sqlWhere = "WHERE nexpose_id ILIKE '#{nexposeId}';" 189 | 190 | @query = @sqlSelect + @sqlWhere 191 | debug_print(@query, debug) 192 | 193 | # Query all nexpose_id's matching the provided vulnerabilities within the vulnotify.yaml configuration file. 194 | @pullVulns = Nexpose::AdhocReportConfig.new(nil, 'sql') 195 | @pullVulns.add_filter('version', '2.0.2') 196 | @pullVulns.add_filter('query', @query) 197 | 198 | # Generate report to be parsed 199 | @pulledVulns = @pullVulns.generate(nsc,18000) 200 | 201 | # http://stackoverflow.com/questions/14199784/convert-csv-file-into-array-of-hashes 202 | # http://technicalpickles.com/posts/parsing-csv-with-ruby/ 203 | # Convert the CSV report information provided by the API back into a hashed format. *Should submit an Idea to Rapid7 for JSON report output type from reports? 204 | @returnedVulns = CSV.parse(@pulledVulns, { :headers => true, :header_converters => :symbol, :converters => [:all, :blank_to_nil] }).map(&:to_hash) if @pulledVulns 205 | 206 | return @returnedVulns 207 | end 208 | 209 | # Using the information from "query_vulns()" query all systems that have the Vulnerability ID associated to it within the specified time range of when it was detected. 210 | def vuln_assets(vulnId, queryTime, lastQueryTime, nsc, debug) 211 | 212 | # Create a base query containing information about assets associated with the provided Vulnerability ID. 213 | @sqlSelect = " 214 | WITH 215 | 216 | asset_names AS ( 217 | SELECT asset_id, array_to_string(array_agg(host_name), ',') AS names 218 | FROM dim_asset_host_name 219 | GROUP BY asset_id 220 | ) 221 | 222 | SELECT DISTINCT ON (asset_id,port) 223 | 224 | asset_id, 225 | ip_address, 226 | port, 227 | dp.name, 228 | mac_address, 229 | host_name, 230 | an.names, 231 | favi.date, 232 | dvs.description, 233 | proofAsText(favi.proof) as proof, 234 | nexpose_id 235 | 236 | FROM fact_asset_vulnerability_instance favi 237 | JOIN dim_asset da USING (asset_id) 238 | JOIN dim_service dsvc USING (service_id) 239 | JOIN dim_protocol dp USING (protocol_id) 240 | JOIN dim_vulnerability_status dvs USING (status_id) 241 | JOIN dim_vulnerability USING (vulnerability_id) 242 | LEFT OUTER JOIN asset_names an USING (asset_id) 243 | LEFT OUTER JOIN dim_scan dsc USING (scan_id) 244 | " 245 | # Provide the Vulnerability ID and time window for the query 246 | @sqlWhere = "WHERE (favi.vulnerability_id = '#{vulnId}') AND (favi.date BETWEEN ('#{lastQueryTime}'::timestamp) and ('#{queryTime}'::timestamp))" 247 | @sqlOrderBy = " ORDER BY asset_id, port;" 248 | @query = @sqlSelect + @sqlWhere + @sqlOrderBy 249 | 250 | @pullVulns = Nexpose::AdhocReportConfig.new(nil, 'sql') 251 | @pullVulns.add_filter('version', '2.0.2') 252 | @pullVulns.add_filter('query', @query) 253 | 254 | # Generate report to be parsed 255 | @pulledVulns = @pullVulns.generate(nsc,18000) 256 | 257 | # http://stackoverflow.com/questions/14199784/convert-csv-file-into-array-of-hashes 258 | # http://technicalpickles.com/posts/parsing-csv-with-ruby/ 259 | # Convert the CSV report information provided by the API back into a hashed format. *Should submit an Idea to Rapid7 for JSON report output type from reports? 260 | @returnedVulns = CSV.parse(@pulledVulns, { :headers => true, :header_converters => :symbol, :converters => [:all, :blank_to_nil] }).map(&:to_hash) if @pulledVulns 261 | 262 | return @returnedVulns 263 | 264 | end 265 | 266 | def vuln_solutions(vulnId, nexposeId, assetId, queryTime, nsc, debug) 267 | 268 | # Create a base query containing information about assets associated with the provided Vulnerability ID. 269 | @sqlSelect = " 270 | SELECT DISTINCT 271 | 272 | ds.summary, 273 | ds.url, 274 | ds.solution_type, 275 | ds.fix, 276 | ds.estimate, 277 | ds.additional_data, 278 | ds.applies_to, 279 | ds.nexpose_id 280 | 281 | FROM fact_asset_vulnerability_instance favi 282 | JOIN dim_asset da USING (asset_id) 283 | JOIN dim_asset_vulnerability_solution davs USING (asset_id, vulnerability_id) 284 | JOIN dim_solution_highest_supercedence dshs USING (solution_id) 285 | JOIN dim_vulnerability dv USING (vulnerability_id) 286 | JOIN dim_solution ds ON ds.solution_id = dshs.superceding_solution_id 287 | 288 | " 289 | # Provide the Vulnerability ID and time window for the query 290 | @sqlWhere = "WHERE (asset_id = #{assetId.to_i} AND vulnerability_id = #{vulnId.to_i})" 291 | @sqlOrderBy = ";" 292 | @query = @sqlSelect + @sqlWhere + @sqlOrderBy 293 | 294 | @pullSols = Nexpose::AdhocReportConfig.new(nil, 'sql') 295 | @pullSols.add_filter('version', '2.0.2') 296 | @pullSols.add_filter('query', @query) 297 | 298 | # Generate report to be parsed 299 | @pulledSols = @pullSols.generate(nsc,18000) 300 | 301 | # http://stackoverflow.com/questions/14199784/convert-csv-file-into-array-of-hashes 302 | # http://technicalpickles.com/posts/parsing-csv-with-ruby/ 303 | # Convert the CSV report information provided by the API back into a hashed format. *Should submit an Idea to Rapid7 for JSON report output type from reports? 304 | @returnedSols = CSV.parse(@pulledSols, { :headers => true, :header_converters => :symbol, :converters => [:all, :blank_to_nil] }).map(&:to_hash) if @pulledSols 305 | 306 | return @returnedSols 307 | 308 | end 309 | 310 | 311 | # For this proof of concept this is one example action that can be taken for an asset that is found to be vulnerable. 312 | def send_notification(mailFrom, mailTo, mailDomain, mailServer, noticeContent, debug) 313 | 314 | # Example Email Notification Template. Modify as needed. Sending HTML email by default because I like it. 315 | # Example Email Notification Template. Modify as needed. Sending HTML email by default because I like it. 316 | message = <#{noticeContent[:date]} - ISO IR Resolve - #{noticeContent[:vulnTitle]} (#{noticeContent[:ipAddress]}) 324 | Link to IDS or other system showing the vulnerability or compromise
325 | https://#{noticeContent[:console]}:#{noticeContent[:conPort]}/vulnerability/vuln-summary.jsp?vulnid=#{noticeContent[:vulnId]}&devid=#{noticeContent[:devId]}
326 | 327 |

328 |

Issue Summary:

329 | A recent scan of #{noticeContent[:ipAddress]} indicates a vulnerability on the system.
330 | The following issue was detected: #{noticeContent[:vulnTitle]} 331 |

Description of the issue:

332 | #{noticeContent[:description]} 333 |

334 | 335 |

336 |

Event Type:

337 | Vulnerable 338 |

339 |

340 |

Host(s) Affected:

341 | #{noticeContent[:ipAddress]}:#{noticeContent[:port]} / #{noticeContent[:proto]}
342 | Hostname: #{noticeContent[:hostName]}
343 | Detected Aliases: #{noticeContent[:otherNames]}
344 | Machine Address: #{noticeContent[:macAddress]}
345 | 346 |

347 |

348 | Time of Detection: #{noticeContent[:date]}
349 | Level of Confidence: #{noticeContent[:confirmation]}
350 |

351 |

Evidence/Testing Results

352 | #{noticeContent[:proof]} 353 | #{noticeContent[:solText]} 354 | 355 |
356 | #{noticeContent[:nexposeId]} 357 | 358 | 359 | MESSAGE_END 360 | 361 | begin 362 | Net::SMTP.start(mailServer) do |smtp| 363 | smtp.send_message message, mailFrom, mailTo 364 | end 365 | 366 | rescue => err 367 | $stderr.puts("Fail: #{err}") 368 | exit(1) 369 | end 370 | end 371 | 372 | def report_vulns(vulnIds, queryTime, lastQueryTime, mailFrom, mailTo, config, nsc, debug) 373 | # Encode HTML entities in output. 374 | coder = HTMLEntities.new 375 | @mailServer = config["mailServer"] 376 | @mailDomain = config["mailDomain"] 377 | @vulnAssets = vuln_assets(vulnIds[:vulnerability_id], queryTime, lastQueryTime, nsc, debug) 378 | 379 | debug_print(@vulnAssets,debug) 380 | 381 | if !@vulnAssets.empty? then # only process an asset list if assets are provided 382 | @vulnAssets.each do |assets| 383 | if assets[:asset_id] then # only process an asset that exists 384 | noticeContent = {} # Ensure noticeContent is cleared 385 | debug_print(assets[:asset_id],debug) 386 | 387 | # This is not terribly efficient but will improve over time. 388 | vulnIds[:description] ? @vulnDescription = "#{vulnIds[:description]}" : @vulnDescription = "A description of this vulnerability is not available for this notice.
" 389 | assets[:proof] ? @proof = "#{coder.encode(assets[:proof])} " : @proof = "No proof provided
" 390 | assets[:mac_address] ? @macAddress = assets[:mac_address] : @macAddress = "No machine address available
" 391 | 392 | @solutions = vuln_solutions(vulnIds[:vulnerability_id], assets[:nexpose_id], assets[:asset_id], assets[:date], nsc, debug) 393 | 394 | @solText = "

Solution Summary:

" 395 | @solutions.each do |sols| 396 | sols[:applies_to] ? @appliesTo = sols[:applies_to] : @appliesTo = "
" 397 | sols[:solution_type] ? @solutionType = "Solution Type: #{sols[:solution_type]}" : @solutionType = "
" 398 | sols[:estimate] ? @estimate = "Estimated remediation time: #{sols[:estimate]}" : @estimate ="No remediation time estimate is available.
" 399 | sols[:summary] ? @solSummary = sols[:summary] : @solSummary = "No summary available
" 400 | sols[:additional_data] ? @additionalData = sols[:additional_data] : @additionalData = "
" 401 | sols[:url] ? @url = sols[:url] : @url = "
" 402 | sols[:fix] ? @fix = sols[:fix] : @fix = "
" 403 | 404 | @solText << " 405 | 406 |

407 |

#{@solSummary}

408 | #{@solutionType} #{@appliesTo} #{@estimate}
409 | #{@url}
410 | #{@fix}
411 | #{@additionalData} 412 |

413 | 414 | " 415 | 416 | end 417 | 418 | noticeContent = { 419 | contact: mailTo, 420 | subject: "Vulnerability Notification", 421 | vulnTitle: "#{vulnIds[:title]}", 422 | vulnId: "#{vulnIds[:vulnerability_id]}", 423 | devId: "#{assets[:asset_id]}", 424 | description: @vulnDescription, 425 | ipAddress: "#{assets[:ip_address]}", 426 | port: "#{assets[:port]}", 427 | proto: "#{assets[:name]}", 428 | macAddress: @macAddress, 429 | hostName: "#{assets[:host_name]}", 430 | otherNames: "#{assets[:names]}", 431 | date: "#{assets[:date]}", 432 | confirmation: "#{assets[:description]}", 433 | proof: @proof, 434 | console: config["hostname"], 435 | conPort: config["port"], 436 | nexposeId: assets[:nexpose_id], 437 | solText: @solText 438 | 439 | # Additional hash values may be added here to provide more information to the notification template. 440 | } 441 | 442 | # Take Action: 443 | # pp(noticeContent.inspect) 444 | # Send an email notification to the default contact for PoC 445 | send_notification(mailFrom, mailTo, @mailDomain, @mailServer, noticeContent, debug) 446 | end 447 | end 448 | end 449 | end 450 | 451 | begin 452 | until 1>2 #horrible keep alive loop... TODO: convert everything to run as part of the NetScan-NG platform 453 | # Reload Configuration in case any vulnerabilies are added or configurations changed 454 | config = YAML.load_file(configPath) 455 | vulNotify = YAML.load_file(vulNotifyPath) 456 | 457 | startTimer = Time.now # Start a timer for how long this process takes 458 | 459 | # specify initial query times for testing 460 | #queryTime = query_time("./tmp/lastRunFile", "2016-10-12 04:03 -400",debug) 461 | #queryTime = query_time("./tmp/lastRunFile", "2016-09-26 22:51 -400",debug) 462 | 463 | queryTime = query_time("./tmp/lastRunFile",nil,debug) 464 | lastQueryTime = last_query_time("./tmp/lastRunFile", ageInterval, queryTime, debug) 465 | 466 | begin 467 | nsc = Nexpose::Connection.new(host, userid, password, port) 468 | begin 469 | checkService(config) 470 | nsc.login 471 | rescue ::Nexpose::APIError => err 472 | $stderr.puts("Connection failed: #{err.reason}") 473 | retry 474 | end 475 | 476 | at_exit {nsc.logout if nsc.session_id} 477 | 478 | # TODO: Complete threading implementation 479 | # Initialize query threads 480 | actionThreads = [] 481 | 482 | vulNotify.each do |vulnToCheck| 483 | debug_print(vulnToCheck,debug) 484 | # Collect information for which vulnerability ID's to evaluate 485 | @returnedVulns = query_vulns(vulnToCheck["vulnId"], nsc, debug).clone 486 | if !@returnedVulns.empty? then # Only process vulnerabilities if they exist. 487 | # Process each returned vulnerability ID 488 | @returnedVulns.each do |vulnIds| 489 | debug_print(vulnIds[:nexpose_id],debug) 490 | # http://stackoverflow.com/questions/1697504/threading-in-ruby-with-a-limit 491 | # until loop waits around until there are less than the specified number of created threads running before allowing execution of the main thread to continue 492 | if !vulnIds.empty? then 493 | until actionThreads.map {|t| t.alive?}.count(true) < threadLimit do sleep 5 end 494 | actionThreads << Thread.new { 495 | report_vulns(vulnIds, queryTime, lastQueryTime, mailFrom, mailTo, config, nsc, debug) if vulnToCheck["reporter_types"].include? 'email' 496 | } 497 | end 498 | end 499 | else 500 | end 501 | end 502 | # The main thread will block until every created thread returns a value. 503 | threadOut = actionThreads.map { |t| t.value } 504 | #actionThreads.each { |t| actionThreads.join } 505 | end 506 | nsc.logout if nsc.session_id # We don't need to stay logged in while we wait for our next run to begin. 507 | 508 | endTimer = Time.now # Stop the timer for how long this process took 509 | runTime = (endTimer - startTimer) # Determine total time taken to run 510 | sleep(ageInterval.hours - runTime) # Sleep for 1 hour from the time the script is started and correct for runtime of the script. 511 | 512 | end 513 | rescue Exception => err 514 | p err 515 | retry 516 | else 517 | 518 | ensure 519 | nsc.logout if nsc.session_id # We don't need to stay logged in while we wait for our next run to begin. 520 | end 521 | 522 | -------------------------------------------------------------------------------- /logtime.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: logtime 3 | # Purpose: Find asset scan times in Nexpose scan logs 4 | # 5 | # Author: Gavin Schneider 6 | # 7 | # Created: 2013-01-29 8 | # Updated: 2013-09-03 9 | # Copyright: (c) Gavin 2013 10 | # Licence: WTFPL 11 | #------------------------------------------------------------------------------- 12 | 13 | 14 | from datetime import datetime, timedelta 15 | from optparse import OptionParser 16 | from itertools import izip 17 | import re 18 | import csv 19 | 20 | #Nexpose log timestamp format, used for converting times 21 | time_format = '%Y-%m-%dT%H:%M:%S' 22 | default_timestamp = timedelta(0) 23 | 24 | #csv headers 25 | headers = ['Site', 'Asset', 'Open TCP Ports', 'Open UDP Ports', 'Discovery Duration', 'URLs Spidered', 'Spider Duration', 'Node Duration', 'Total Duration', 'Completed', 'TCP Port List', 'UDP Port List', 'Vulnerabilities', 'Fingerprint Certainty'] 26 | summary_headers = ['Site', 'Assets Logged', 'Live Assets', 'Assets Scanned', 'Total Scan Duration', 'High Duration Asset', 'High Duration', 'Low Duration Asset', 'Low Duration'] 27 | 28 | #match site name 29 | sitePattern = re.compile('\[Site: (?P.*?)\]') 30 | #match scan start / pause / stop (TODO: use this to split a nse.log) 31 | scanStartPattern = re.compile('Scan for site') 32 | scanPausePattern = re.compile('Scan paused') 33 | scanStopPattern = re.compile('\[Site: .*?\] (Scan stopped|Scan completed)') 34 | #should match any ipv4 address (now constrained to be within brackets) 35 | ipPattern = re.compile('[:\[](?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[:\]]') 36 | ipPattern2 = re.compile('[:\[|\[Target: ](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[:\]]') 37 | #matches Nexpose log timestamp format 38 | timePattern = re.compile('^(19[0-9]{2}|2[0-9]{3})-(0[1-9]|1[012])-([123]0|[012][1-9]|31)T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])') 39 | #matches keywords in Nexpose logs 40 | startPattern = re.compile('starting node scan') 41 | endPattern = re.compile('Freeing node cache data') 42 | #updated nmap log message regexes 43 | alivePattern = re.compile('\[(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\] ALIVE \(reason=(.*?):') 44 | deadPattern = re.compile('\[(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\] DEAD \(reason=(.*?)\)') 45 | tcpPattern = re.compile('\[(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(?P\d{0,5})/TCP\] OPEN \(reason=(?P.*?):') 46 | udpPattern = re.compile('\[(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(?P\d{0,5})/UDP\] OPEN \(reason=(?P.*?):') 47 | udp2Pattern = re.compile('maybe open UDP ports') 48 | spiderStartPattern = re.compile('\[Thread: SPIDER::do-http-spiderv2-setup@\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\] \[Site: (?P.*?)\] \[(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(?P\d{1,5})\]') 49 | spiderEndPattern = re.compile('\[Thread: .*?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\] \[Site: (?P.*?)\] \[(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\] Closing service: Ne[Xx]poseWebSpider\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:(?P\d{1,5})\] \(source: (?P.*?)\)') 50 | spiderSummaryPattern = re.compile('\[Thread: .*?:(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\] \[Site: (?P.*?)\] Shutting down spider \((?P.*?) URLs spidered in') 51 | sysFingerprintPattern = re.compile('\[(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\] (?P.*?) SystemFingerprint.*?\[certainty=(?P.*?)\]\[description=(?P.*?)\].*? source: (?P.*?)$') 52 | vulnerablePattern = re.compile('- VULNERABLE') 53 | 54 | def main(): 55 | 56 | def verbose_output(site, message, timestamp): 57 | """Print or write to file a verbose message when verbose output is enabled.""" 58 | verbosetext = '[Site: {0}] {1} at {2}'.format(site, message, timestamp) 59 | if options.verbose: 60 | print verbosetext 61 | if options.outverbose: 62 | outf.write(verbosetext + '\n') 63 | 64 | def init_asset(site, timestamp): 65 | """Create dictionary for a newly added asset""" 66 | asset_dict = {'sitename':site,'alive': '','tcptime':'','tcpports':0,'tcpportlist':[],'udptime':'','udpports':0,'udpportlist':[],'udpmaybeports':'','udpmaybeportlist':'','nodestart':'','nodeend':'','spiderstart':'','spiderend':'','urls':0, 'last_timestamp':timestamp, 'first_timestamp':timestamp, 'completed': 'No', 'vulns':0, 'fingerprint_certainty':''} 67 | return asset_dict 68 | 69 | def init_site(timestamp): 70 | """Create dictionary for a newly added site""" 71 | site_dict = {'scan_start':[], 'scan_pause':[], 'scan_stop':[], 'scan_durations':[], 'scan_total_duration':'', 'last_timestamp':timestamp, 'first_timestamp':timestamp, 'dead_ips':[], 'completed': 'No'} 72 | return site_dict 73 | 74 | def calc_duration(start, end): 75 | """Calculate the duration between two timestamps""" 76 | duration = datetime.strptime(end, time_format) - datetime.strptime(start, time_format) 77 | return duration 78 | 79 | usage = "usage: %prog [options]" 80 | parser = OptionParser(usage) 81 | parser.add_option("-o", "--out", dest="outfile", help="Output results to flat text FILE (optional).", metavar="FILE") 82 | #todo: implement csv output 83 | parser.add_option("-c", "--csv", dest="csvfile", help="Output results to CSV FILE (optional)", metavar="FILE") 84 | parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Enable verbose console output. Warning: very spammy!") 85 | parser.add_option("-u", "--outverbose", dest="outverbose", default=False, help="Enable verbose file output. Warning: very spammy!", metavar="FILE") 86 | parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Only show brief summary in console output.") 87 | 88 | (options, args) = parser.parse_args() 89 | 90 | if len(args) < 1: 91 | parser.error("A log file name is required as input.") 92 | else: 93 | filename = args[0] 94 | if options.quiet: 95 | options.verbose = False 96 | 97 | try: 98 | #if we're writing verbose output, let's open the specified file for writing 99 | if options.outverbose: 100 | outf = open(options.outverbose, 'wb') 101 | 102 | #the magic begins - open file as read-only, and binary mode due to Windows bug with special chars 103 | with open(filename, 'rb') as f: 104 | sitedata = {} 105 | assetdata = {} 106 | for line in f: 107 | #check to see if any regexes match in the current line of the log file 108 | ip = ipPattern.search(line) 109 | alive = alivePattern.search(line) 110 | dead = deadPattern.search(line) 111 | tcp_port = tcpPattern.search(line) 112 | udp_port = udpPattern.search(line) 113 | node_start = startPattern.search(line) 114 | node_end = endPattern.search(line) 115 | spider_start = spiderStartPattern.search(line) 116 | spider_end = spiderEndPattern.search(line) 117 | scan_start = scanStartPattern.search(line) 118 | scan_pause = scanPausePattern.search(line) 119 | scan_stop = scanStopPattern.search(line) 120 | system_fingerprint = sysFingerprintPattern.search(line) 121 | log_timestamp = timePattern.search(line) 122 | site_name = sitePattern.search(line) 123 | spider_summary = spiderSummaryPattern.search(line) 124 | vuln = vulnerablePattern.search(line) 125 | 126 | if log_timestamp: 127 | timestamp = log_timestamp.group() 128 | 129 | if site_name: 130 | sitename = site_name.group(1) 131 | if sitename in sitedata: 132 | sitedata[sitename]['last_timestamp'] = timestamp 133 | else: 134 | sitedata[sitename] = init_site(timestamp) 135 | sitedata[sitename]['first_timestamp'] = timestamp 136 | verbose_output(sitename, 'found in log', timestamp) 137 | 138 | if scan_start: 139 | verbose_output(sitename, 'scan STARTED', timestamp) 140 | if sitename in sitedata: 141 | sitedata[sitename]['scan_start'].append(timestamp) 142 | else: 143 | sitedata[sitename] = init_site(timestamp) 144 | sitedata[sitename]['scan_start'].append(timestamp) 145 | 146 | if scan_pause: 147 | verbose_output(sitename, 'scan PAUSED', timestamp) 148 | if sitename in sitedata: 149 | sitedata[sitename]['scan_pause'].append(timestamp) 150 | else: 151 | sitedata[sitename] = init_site(timestamp) 152 | sitedata[sitename]['scan_pause'].append(timestamp) 153 | 154 | if scan_stop: 155 | verbose_output(sitename, 'scan STOPPED', timestamp) 156 | if sitename in sitedata: 157 | sitedata[sitename]['scan_stop'].append(timestamp) 158 | sitedata[sitename]['completed'] = 'Yes' 159 | else: 160 | sitedata[sitename] = init_site(timestamp) 161 | sitedata[sitename]['scan_stop'].append(timestamp) 162 | sitedata[sitename]['completed'] = 'Yes' 163 | 164 | if ip: 165 | ip = ip.group(1) 166 | if ip in assetdata: 167 | assetdata[ip]['last_timestamp'] = timestamp 168 | elif not dead: 169 | assetdata[ip] = init_asset(sitename, timestamp) 170 | if not alive: 171 | verbose_output(sitename, 'Asset {0} found in log before ALIVE status'.format(ip), timestamp) 172 | 173 | if alive: 174 | if ip in assetdata: 175 | assetdata[ip]['alive'] = timestamp 176 | verbose_output(sitename, 'Asset {0} found ALIVE'.format(ip), timestamp) 177 | else: 178 | assetdata[ip] = init_asset(sitename, timestamp) 179 | assetdata[ip]['alive'] = timestamp 180 | verbose_output(sitename, 'Asset {0} found ALIVE'.format(ip), timestamp) 181 | 182 | if dead: 183 | sitedata[sitename]['dead_ips'].append(ip) 184 | verbose_output(sitename, 'Asset {0} found DEAD'.format(ip), timestamp) 185 | 186 | if tcp_port: 187 | tcpport = tcp_port.group('port') 188 | tcpreason = tcp_port.group('reason') 189 | assetdata[ip]['tcpportlist'].append(tcpport) 190 | assetdata[ip]['tcpports'] = len(assetdata[ip]['tcpportlist']) 191 | if not assetdata[ip]['tcptime']: 192 | assetdata[ip]['tcptime'] = timestamp 193 | verbose_output(sitename, 'Asset {0} found open TCP port {1} reason: {2}'.format(ip, tcpport, tcpreason),timestamp) 194 | 195 | if udp_port: 196 | udpport = udp_port.group('port') 197 | udpreason = udp_port.group('reason') 198 | assetdata[ip]['udpportlist'].append(udpport) 199 | assetdata[ip]['udpports'] = len(assetdata[ip]['udpportlist']) 200 | if not assetdata[ip]['udptime']: 201 | assetdata[ip]['udptime'] = timestamp 202 | verbose_output(sitename, 'Asset {0} found open UDP port {1} reason: {2}'.format(ip, udpport, udpreason),timestamp) 203 | 204 | if node_start: 205 | if ip and log_timestamp: 206 | assetdata[ip]['nodestart'] = timestamp 207 | verbose_output(sitename, 'Asset {0} node scan started'.format(ip), timestamp) 208 | 209 | if node_end: 210 | if ip and log_timestamp: 211 | assetdata[ip]['nodeend'] = timestamp 212 | assetdata[ip]['completed'] = 'Yes' 213 | verbose_output(sitename, 'Asset {0} node scan ended'.format(ip), timestamp) 214 | 215 | if spider_start: 216 | if ip and log_timestamp: 217 | if not assetdata[ip]['spiderstart']: 218 | assetdata[ip]['spiderstart'] = timestamp 219 | verbose_output(sitename, 'Asset {0} web spider started'.format(ip), timestamp) 220 | 221 | if spider_end: 222 | if ip and log_timestamp: 223 | assetdata[ip]['spiderend'] = timestamp 224 | verbose_output(sitename, 'Asset {0} web spider ended'.format(ip), timestamp) 225 | 226 | if spider_summary: 227 | if ip and log_timestamp: 228 | if not assetdata[ip]['urls'] or assetdata[ip]['urls'] < spider_summary.group('urls'): 229 | assetdata[ip]['urls'] = spider_summary.group('urls') 230 | if vuln: 231 | if ip: 232 | assetdata[ip]['vulns'] += 1 233 | 234 | if system_fingerprint: 235 | if ip: 236 | assetdata[ip]['fingerprint_certainty'] = system_fingerprint.group('certainty') 237 | verbose_output(sitename, 'Asset {0} fingerprint certainty: {1}'.format(ip, system_fingerprint.group('certainty')), timestamp) 238 | 239 | except KeyboardInterrupt: 240 | print '\nExit: Interrupted by user.' 241 | exit(0) 242 | 243 | #open a specified plaintext file for writing 244 | if not options.outverbose: 245 | if options.outfile: 246 | outf = open(options.outfile, 'wb') 247 | 248 | #todo: implement CSV output (in particular, summary csv file) 249 | #open a specified CSV file for writing 250 | if options.csvfile: 251 | #outcsvsum = ''.join((options.csvfile.rstrip('.csv'),'_summary.csv')) 252 | outc = open(options.csvfile, 'wb') 253 | #outcs = open(outcsvsum, 'w') 254 | csvwriter = csv.writer(outc) 255 | #csvsumwriter = csv.writer(outcs) 256 | csvwriter.writerow(headers) 257 | #csvsumwriter.write(summary_headers) 258 | 259 | try: 260 | #total up scan durations for each site found in scan log 261 | for site in sitedata: 262 | starts = len(sitedata[site]['scan_start']) 263 | pauses = len(sitedata[site]['scan_pause']) 264 | stops = len(sitedata[site]['scan_stop']) 265 | 266 | if pauses >= 1: 267 | if starts >= pauses: 268 | for start, pause in izip(sitedata[site]['scan_start'], sitedata[site]['scan_pause']): 269 | sitedata[site]['scan_durations'].append(calc_duration(start, pause)) 270 | 271 | if stops <= 0: 272 | sitedata[site]['scan_stop'].append(sitedata[site]['last_timestamp']) 273 | 274 | sitedata[site]['scan_durations'].append(calc_duration(sitedata[site]['scan_start'][starts - 1], sitedata[site]['scan_stop'][stops - 1])) 275 | 276 | total = timedelta(0) 277 | for duration in sitedata[site]['scan_durations']: 278 | total = total + duration 279 | 280 | sitedata[site]['scan_total_duration'] = total 281 | longestscan = {'site':'','asset':'','time':timedelta(0)} 282 | shortestscan = {'site':'','asset':'','time':timedelta(365)} 283 | alivecount = 0 284 | scannedcount = 0 285 | node_times = [] 286 | discovery_times = [] 287 | spider_times = [] 288 | 289 | for asset in assetdata: 290 | if assetdata[asset]['sitename'] == site: 291 | if assetdata[asset]['nodeend'] and assetdata[asset]['nodestart']: 292 | assetdata[asset]['nodetime'] = calc_duration(assetdata[asset]['nodestart'], assetdata[asset]['nodeend']) 293 | node_times.append(assetdata[asset]['nodetime']) 294 | elif assetdata[asset]['nodestart']: 295 | assetdata[asset]['nodetime'] = calc_duration(assetdata[asset]['nodestart'], assetdata[asset]['last_timestamp']) 296 | node_times.append(assetdata[asset]['nodetime']) 297 | else: 298 | assetdata[asset]['nodetime'] = 'Unknown' 299 | 300 | if assetdata[asset]['alive']: 301 | assetdata[asset]['discoverytime'] = calc_duration(sitedata[site]['scan_start'][0], assetdata[asset]['alive']) 302 | discovery_times.append(assetdata[asset]['discoverytime']) 303 | elif assetdata[asset]['tcptime']: 304 | assetdata[asset]['discoverytime'] = calc_duration(sitedata[site]['scan_start'][0], assetdata[asset]['tcptime']) 305 | discovery_times.append(assetdata[asset]['discoverytime']) 306 | elif assetdata[asset]['udptime']: 307 | assetdata[asset]['discoverytime'] = calc_duration(sitedata[site]['scan_start'][0], assetdata[asset]['udptime']) 308 | discovery_times.append(assetdata[asset]['discoverytime']) 309 | else: 310 | assetdata[asset]['discoverytime'] = 'Unknown' 311 | 312 | if assetdata[asset]['spiderend'] and assetdata[asset]['spiderstart']: 313 | assetdata[asset]['spidertime'] = calc_duration(assetdata[asset]['spiderstart'], assetdata[asset]['spiderend']) 314 | spider_times.append(assetdata[asset]['spidertime']) 315 | elif assetdata[asset]['spiderstart']: 316 | assetdata[asset]['spidertime'] = calc_duration(assetdata[asset]['spiderstart'], assetdata[asset]['last_timestamp']) 317 | spider_times.append(assetdata[asset]['spidertime']) 318 | else: 319 | assetdata[asset]['spidertime'] = 'Unknown' 320 | 321 | if assetdata[asset]['discoverytime'] != 'Unknown' and assetdata[asset]['nodetime'] != 'Unknown': 322 | assetdata[asset]['totaltime'] = assetdata[asset]['discoverytime'] + assetdata[asset]['nodetime'] 323 | else: 324 | assetdata[asset]['totaltime'] = 'Unknown' 325 | 326 | if assetdata[asset]['totaltime'] != 'Unknown' and assetdata[asset]['totaltime'] > longestscan['time']: 327 | longestscan['site'] = assetdata[asset]['sitename'] 328 | longestscan['asset'] = asset 329 | longestscan['time'] = assetdata[asset]['totaltime'] 330 | 331 | if assetdata[asset]['totaltime'] != 'Unknown' and assetdata[asset]['totaltime'] < shortestscan['time']: 332 | shortestscan['site'] = assetdata[asset]['sitename'] 333 | shortestscan['asset'] = asset 334 | shortestscan['time'] = assetdata[asset]['totaltime'] 335 | 336 | if assetdata[asset]['alive']: 337 | alivecount += 1 338 | 339 | if assetdata[asset]['nodeend']: 340 | scannedcount += 1 341 | 342 | #outtext = 'Site: %s | Asset: %s | Open Ports: %s | Discovery Time: %s | Spider Time: %s | Node Time: %s | Total Time: %s' % (assetdata[asset]['sitename'],asset.ljust(15), str(assetdata[asset]['tcpports']).ljust(5), str(assetdata[asset]['discoverytime']).ljust(17), str(assetdata[asset]['spidertime']).ljust(17), str(assetdata[asset]['nodetime']).ljust(17), str(assetdata[asset]['totaltime'])) 343 | 344 | if not options.quiet: 345 | #print outtext 346 | pass 347 | 348 | if options.outfile: 349 | #outf.write(outtext + '\n') 350 | pass 351 | 352 | if options.csvfile: 353 | csvwriter.writerow((assetdata[asset]['sitename'], asset, assetdata[asset]['tcpports'], assetdata[asset]['udpports'], str(assetdata[asset]['discoverytime']), str(assetdata[asset]['urls']), str(assetdata[asset]['spidertime']), str(assetdata[asset]['nodetime']), str(assetdata[asset]['totaltime']), assetdata[asset]['completed'], ', '.join(assetdata[asset]['tcpportlist']), ', '.join(assetdata[asset]['udpportlist']), str(assetdata[asset]['vulns']), str(assetdata[asset]['fingerprint_certainty']) )) 354 | 355 | 356 | if discovery_times: 357 | average_discovery_time = sum(discovery_times, default_timestamp) / len(discovery_times) 358 | else: 359 | average_discovery_time = 'Unknown' 360 | if node_times: 361 | average_node_time = sum(node_times, default_timestamp) / len(node_times) 362 | else: 363 | average_node_time = 'Unknown' 364 | if spider_times: 365 | average_spider_time = sum(spider_times, default_timestamp) / len(spider_times) 366 | else: 367 | average_spider_time = 'Unknown' 368 | 369 | print '\nSummary for [Site: %s]' % site 370 | print 'Total assets logged: %i' % ((len(assetdata)+len(sitedata[sitename]['dead_ips']))) 371 | print 'Total assets alive: %i' % (alivecount) 372 | print 'Total assets scanned (complete): %i' % (scannedcount) 373 | print 'Total scan time: %s' % total 374 | print 'Scan completed: %s' % sitedata[site]['completed'] 375 | if longestscan['site']: 376 | print 'Most scan time: %s @ %s' % (longestscan['asset'], longestscan['time']) 377 | if shortestscan['site']: 378 | print 'Least scan time: %s @ %s \n' % (shortestscan['asset'], shortestscan['time']) 379 | print 'Average discovery time: %s' % average_discovery_time 380 | print 'Average node time: %s' % average_node_time 381 | print 'Average web spider time: %s' % average_spider_time 382 | 383 | #todo: make sure each site detected can be summarized 384 | #csvsumwriter.write(site, logged_assets, live_assets, scanned_assets, total_duration, high_duration_asset, high_duration, low_duration_asset, low_duration) 385 | 386 | except KeyboardInterrupt: 387 | if options.outfile: 388 | outf.close() 389 | if options.csvfile: 390 | outc.close() 391 | #outcs.close() 392 | print '\nExit: Interrupted by user' 393 | exit(0) 394 | #close said file after writing 395 | if options.outfile: 396 | outf.close() 397 | if options.csvfile: 398 | outc.close() 399 | #outcs.close() 400 | 401 | 402 | if __name__ == '__main__': 403 | main() --------------------------------------------------------------------------------