├── .gitignore ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── examples ├── output.json ├── rest.rb ├── rest │ └── helpers.rb └── rpc.rb ├── lib └── peplum │ ├── nmap.rb │ └── nmap │ ├── payload.rb │ ├── services │ ├── info.rb │ └── rest_proxy.rb │ └── version.rb └── peplum-nmap.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | /.bundle/ 3 | /.yardoc 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in peplum-nmap.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Copyright (c) 2023 Ecsypno Single Member P.C. . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Peplum::Nmap 2 | 3 | Peplum::Nmap is a distributed network mapper/security scanner backed by: 4 | 5 | * [Peplum](https://github.com/peplum/peplum) for the distributed architecture. 6 | * [nmap](https://nmap.org/) for the scanning engine. 7 | * [ruby-nmap](https://github.com/postmodern/ruby-nmap) for the Ruby middleware. 8 | 9 | Its basic function is to distribute the scanning of IP ranges across multiple machines and thus parallelize an otherwise 10 | quite time consuming task. 11 | 12 | ## Installation 13 | 14 | ### Source 15 | 16 | $ git clone git@github.com:peplum/peplum-nmap.git 17 | $ cd peplum-nmap 18 | $ bundle install 19 | 20 | ### Gem 21 | 22 | $ gem install peplum-nmap 23 | 24 | ## Usage 25 | 26 | See the `examples/` directory. 27 | 28 | ### Grid 29 | 30 | Peplum::Nmap can initiate scans from the same machine, but the idea behind it is to use a _Grid_ which transparently load-balances 31 | and line-aggregates, in order to combine resources and perform a faster scan than one single machine could. 32 | 33 | That _Grid_ technology is graciously provided by [Peplum](https://github.com/peplum/peplum) and can be setup like so: 34 | 35 | ``` 36 | $ bundle exec irb 37 | irb(main):001:0> require 'peplum/nmap' 38 | => true 39 | irb(main):002:0> Peplum::Nmap::Application.spawn( :agent, address: Socket.gethostname ) 40 | I, [2023-05-21T19:11:20.772790 #359147] INFO -- System: Logfile at: /home/zapotek/.cuboid/logs/Agent-359147-8499.log 41 | I, [2023-05-21T19:11:20.772886 #359147] INFO -- System: [PID 359147] RPC Server started. 42 | I, [2023-05-21T19:11:20.772892 #359147] INFO -- System: Listening on xps:8499 43 | ``` 44 | 45 | And at the terminal of another machine: 46 | 47 | ``` 48 | $ bundle exec irb 49 | irb(main):001:0> require 'peplum/nmap' 50 | => true 51 | irb(main):002:0> Peplum::Nmap::Application.spawn( :agent, address: Socket.gethostname, peer: 'xps:8499' ) 52 | I, [2023-05-21T19:12:38.897746 #359221] INFO -- System: Logfile at: /home/zapotek/.cuboid/logs/Agent-359221-5786.log 53 | I, [2023-05-21T19:12:38.998472 #359221] INFO -- System: [PID 359221] RPC Server started. 54 | I, [2023-05-21T19:12:38.998494 #359221] INFO -- System: Listening on xps:5786 55 | ``` 56 | 57 | That's a _Grid_ of 2 Peplum::Nmap _Agents_, both of them available to provide scanner _Instances_ that can be used to parallelize 58 | network mapping/security scans. 59 | 60 | If those 2 machines use a different pipe to the network you wish to scan, the result will be that the network resources 61 | are going to be in a way combined; or if the scan is too CPU intensive for just one machine, this will split the workload 62 | amongst the 2. 63 | 64 | The cool thing is that it doesn't matter to which you refer for _Instance_ _spawning_, the appropriate one is going to 65 | be the one providing it. 66 | 67 | You can then configure the _REST_ service to use any of those 2 _Agents_ and perform your scan -- 68 | see [examples/rest.rb](https://github.com/peplum/peplum-nmap/blob/master/examples/rest.rb). 69 | 70 | The _REST_ service is good for integration, so it's your safe bet; you can however also take advantage of the internal 71 | _RPC_ protocol and opt for something more like [examples/rpc.rb](https://github.com/peplum/peplum-nmap/blob/master/examples/rpc.rb). 72 | 73 | ## Contributing 74 | 75 | Bug reports and pull requests are welcome on GitHub at https://github.com/peplum/peplum-nmap. 76 | 77 | ## Funding 78 | 79 | Peplum::Nmap is a [Peplum](https://github.com/peplum/) project and as such funded by [Ecsypno Single Member P.C.](https://ecsypno.com). 80 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | task default: %i[] 5 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "peplum/nmap" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /examples/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosts": { 3 | "192.168.1.1": { 4 | "start_time": "2023-05-21 19:30:39 +0300", 5 | "end_time": "2023-05-21 19:31:20 +0300", 6 | "status": "up", 7 | "addresses": [ 8 | "192.168.1.1" 9 | ], 10 | "mac": null, 11 | "vendor": null, 12 | "ipv4": "192.168.1.1", 13 | "ipv6": null, 14 | "hostname": "mywebui.net", 15 | "hostnames": [ 16 | "mywebui.net" 17 | ], 18 | "os": null, 19 | "uptime": null, 20 | "ports": { 21 | "53": { 22 | "protocol": "tcp", 23 | "state": "open", 24 | "reason": "syn-ack", 25 | "reason_ttl": "syn-ack", 26 | "service": "dnsmasq 2.85", 27 | "scripts": { 28 | "dns-nsid": { 29 | "output": "\n bind.version: dnsmasq-2.85", 30 | "data": { 31 | "bind.version": "dnsmasq-2.85" 32 | } 33 | } 34 | } 35 | }, 36 | "80": { 37 | "protocol": "tcp", 38 | "state": "open", 39 | "reason": "syn-ack", 40 | "reason_ttl": "syn-ack", 41 | "service": "http", 42 | "scripts": { 43 | "http-server-header": { 44 | "output": "httpd/2.7 (Netgear; D86)", 45 | "data": [ 46 | "httpd/2.7 (Netgear; D86)" 47 | ] 48 | }, 49 | "http-title": { 50 | "output": "NETGEAR\nRequested resource was http://mywebui.net/index.html?sessionId=00000005%2DqJnmUh51Zh2tSmWlMkdD4dXOUxF7xn3", 51 | "data": { 52 | "title": "NETGEAR", 53 | "redirect_url": "http://mywebui.net/index.html?sessionId=00000005%2DqJnmUh51Zh2tSmWlMkdD4dXOUxF7xn3" 54 | } 55 | } 56 | } 57 | }, 58 | "5510": { 59 | "protocol": "tcp", 60 | "state": "filtered", 61 | "reason": "no-response", 62 | "reason_ttl": "no-response", 63 | "service": "secureidprop", 64 | "scripts": { 65 | } 66 | } 67 | } 68 | }, 69 | "192.168.1.43": { 70 | "start_time": "2023-05-21 19:30:40 +0300", 71 | "end_time": "2023-05-21 19:33:15 +0300", 72 | "status": "up", 73 | "addresses": [ 74 | "192.168.1.43" 75 | ], 76 | "mac": null, 77 | "vendor": null, 78 | "ipv4": "192.168.1.43", 79 | "ipv6": null, 80 | "hostname": "deco_X60.net", 81 | "hostnames": [ 82 | "deco_X60.net" 83 | ], 84 | "os": null, 85 | "uptime": null, 86 | "ports": { 87 | "80": { 88 | "protocol": "tcp", 89 | "state": "open", 90 | "reason": "syn-ack", 91 | "reason_ttl": "syn-ack", 92 | "service": "http", 93 | "scripts": { 94 | "fingerprint-strings": { 95 | "output": "\n FourOhFourRequest: \n HTTP/1.0 404 Not Found\n Connection: close\n Content-Type: text/html\n

Not Found

The requested URL /nice%20ports%2C/Tri%6Eity.txt%2ebak was not found on this server.\n GetRequest, HTTPOptions: \n HTTP/1.0 200 OK\n Connection: close\n ETag: \"86b-110-5ce64ee5\"\n Last-Modified: Thu, 23 May 2019 07:42:29 GMT\n Date: Sun, 21 May 2023 16:30:46 GMT\n X-Frame-Options: deny\n Content-Security-Policy: frame-ancestors 'none'\n Content-Type: text/html\n Content-Length: 272\n \n \n \n \n \n \n \n Help: \n HTTP/0.9 400 Bad Request\n Connection: Keep-Alive\n Keep-Alive: timeout=20\n Content-Type: text/html\n

Bad Request

\n RTSPRequest: \n HTTP/1.0 400 Bad Request\n Connection: Keep-Alive\n Keep-Alive: timeout=20\n Content-Type: text/html\n

Bad Request

", 96 | "data": { 97 | "FourOhFourRequest": "\n HTTP/1.0 404 Not Found\n Connection: close\n Content-Type: text/html\n

Not Found

The requested URL /nice%20ports%2C/Tri%6Eity.txt%2ebak was not found on this server.", 98 | "GetRequest, HTTPOptions": "\n HTTP/1.0 200 OK\n Connection: close\n ETag: \"86b-110-5ce64ee5\"\n Last-Modified: Thu, 23 May 2019 07:42:29 GMT\n Date: Sun, 21 May 2023 16:30:46 GMT\n X-Frame-Options: deny\n Content-Security-Policy: frame-ancestors 'none'\n Content-Type: text/html\n Content-Length: 272\n \n \n \n \n \n \n ", 99 | "Help": "\n HTTP/0.9 400 Bad Request\n Connection: Keep-Alive\n Keep-Alive: timeout=20\n Content-Type: text/html\n

Bad Request

", 100 | "RTSPRequest": "\n HTTP/1.0 400 Bad Request\n Connection: Keep-Alive\n Keep-Alive: timeout=20\n Content-Type: text/html\n

Bad Request

" 101 | } 102 | }, 103 | "http-title": { 104 | "output": "Site doesn't have a title (text/html).", 105 | "data": null 106 | } 107 | } 108 | }, 109 | "443": { 110 | "protocol": "tcp", 111 | "state": "open", 112 | "reason": "syn-ack", 113 | "reason_ttl": "syn-ack", 114 | "service": "https", 115 | "scripts": { 116 | "http-title": { 117 | "output": "Site doesn't have a title (text/html).", 118 | "data": null 119 | }, 120 | "ssl-cert": { 121 | "output": "Subject: commonName=tplinkdeco.net/countryName=CN\nNot valid before: 2010-01-01T00:00:00\nNot valid after: 2030-12-31T00:00:00", 122 | "data": { 123 | "subject": { 124 | "countryName": "CN", 125 | "commonName": "tplinkdeco.net" 126 | }, 127 | "issuer": { 128 | "countryName": "CN", 129 | "commonName": "tplinkdeco.net" 130 | }, 131 | "pubkey": { 132 | "exponent": "BIGNUM: 0x5574815bd108", 133 | "bits": "2048", 134 | "modulus": "BIGNUM: 0x5574815bd148", 135 | "type": "rsa" 136 | }, 137 | "extensions": [ 138 | { 139 | "name": "X509v3 Basic Constraints", 140 | "value": "CA:FALSE" 141 | }, 142 | { 143 | "name": "Netscape Comment", 144 | "value": "OpenSSL Generated Certificate" 145 | }, 146 | { 147 | "name": "X509v3 Subject Key Identifier", 148 | "value": "9D:23:AE:93:2D:A9:BB:3D:F1:4B:1E:5B:23:C5:86:30:C9:2E:B3:23" 149 | }, 150 | { 151 | "name": "X509v3 Authority Key Identifier", 152 | "value": "11:D9:C7:7D:04:5B:F3:B3:B7:2A:3C:02:56:88:75:63:48:A4:47:BB" 153 | } 154 | ], 155 | "validity": { 156 | "notAfter": "2030-12-31T00:00:00", 157 | "notBefore": "2010-01-01T00:00:00" 158 | } 159 | } 160 | }, 161 | "ssl-date": { 162 | "output": "TLS randomness does not represent time", 163 | "data": null 164 | } 165 | } 166 | }, 167 | "1900": { 168 | "protocol": "tcp", 169 | "state": "open", 170 | "reason": "syn-ack", 171 | "reason_ttl": "syn-ack", 172 | "service": "MiniUPnP 1.8", 173 | "scripts": { 174 | "fingerprint-strings": { 175 | "output": "\n FourOhFourRequest, GetRequest: \n HTTP/1.0 404 Not Found\n Content-Type: text/html\n Connection: close\n Content-Length: 134\n Server: TP-LINK/TP-LINK UPnP/1.1 MiniUPnPd/1.8\n Ext:\n 404 Not Found

Not Found

The requested URL was not found on this server.\n GenericLines: \n 501 Not Implemented\n Content-Type: text/html\n Connection: close\n Content-Length: 149\n Server: TP-LINK/TP-LINK UPnP/1.1 MiniUPnPd/1.8\n Ext:\n 501 Not Implemented

Not Implemented

The HTTP Method is not implemented by this server.\n HTTPOptions: \n HTTP/1.0 501 Not Implemented\n Content-Type: text/html\n Connection: close\n Content-Length: 149\n Server: TP-LINK/TP-LINK UPnP/1.1 MiniUPnPd/1.8\n Ext:\n 501 Not Implemented

Not Implemented

The HTTP Method is not implemented by this server.\n RTSPRequest: \n RTSP/1.0 501 Not Implemented\n Content-Type: text/html\n Connection: close\n Content-Length: 149\n Server: TP-LINK/TP-LINK UPnP/1.1 MiniUPnPd/1.8\n Ext:\n 501 Not Implemented

Not Implemented

The HTTP Method is not implemented by this server.\n SIPOptions: \n SIP/2.0 501 Not Implemented\n Content-Type: text/html\n Connection: close\n Content-Length: 149\n Server: TP-LINK/TP-LINK UPnP/1.1 MiniUPnPd/1.8\n Ext:\n 501 Not Implemented

Not Implemented

The HTTP Method is not implemented by this server.", 176 | "data": { 177 | "FourOhFourRequest, GetRequest": "\n HTTP/1.0 404 Not Found\n Content-Type: text/html\n Connection: close\n Content-Length: 134\n Server: TP-LINK/TP-LINK UPnP/1.1 MiniUPnPd/1.8\n Ext:\n 404 Not Found

Not Found

The requested URL was not found on this server.", 178 | "GenericLines": "\n 501 Not Implemented\n Content-Type: text/html\n Connection: close\n Content-Length: 149\n Server: TP-LINK/TP-LINK UPnP/1.1 MiniUPnPd/1.8\n Ext:\n 501 Not Implemented

Not Implemented

The HTTP Method is not implemented by this server.", 179 | "HTTPOptions": "\n HTTP/1.0 501 Not Implemented\n Content-Type: text/html\n Connection: close\n Content-Length: 149\n Server: TP-LINK/TP-LINK UPnP/1.1 MiniUPnPd/1.8\n Ext:\n 501 Not Implemented

Not Implemented

The HTTP Method is not implemented by this server.", 180 | "RTSPRequest": "\n RTSP/1.0 501 Not Implemented\n Content-Type: text/html\n Connection: close\n Content-Length: 149\n Server: TP-LINK/TP-LINK UPnP/1.1 MiniUPnPd/1.8\n Ext:\n 501 Not Implemented

Not Implemented

The HTTP Method is not implemented by this server.", 181 | "SIPOptions": "\n SIP/2.0 501 Not Implemented\n Content-Type: text/html\n Connection: close\n Content-Length: 149\n Server: TP-LINK/TP-LINK UPnP/1.1 MiniUPnPd/1.8\n Ext:\n 501 Not Implemented

Not Implemented

The HTTP Method is not implemented by this server." 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /examples/rest.rb: -------------------------------------------------------------------------------- 1 | require 'peplum/nmap' 2 | require 'pp' 3 | require_relative 'rest/helpers' 4 | 5 | # Boot up our REST nmap server for easy integration. 6 | rest_pid = Peplum::Nmap::Application.spawn( :rest, daemonize: true ) 7 | at_exit { Cuboid::Processes::Manager.kill rest_pid } 8 | 9 | # Wait for the REST server to boot up. 10 | while sleep 1 11 | begin 12 | request :get 13 | rescue Errno::ECONNREFUSED 14 | next 15 | end 16 | 17 | break 18 | end 19 | 20 | # Assign an nmap Agent to the REST service for it to provide us with scanner Instances. 21 | nmap_agent = Peplum::Nmap::Application.spawn( :agent, daemonize: true ) 22 | request :put, 'agent/url', nmap_agent.url 23 | at_exit { nmap_agent.shutdown rescue nil } 24 | 25 | # Create a new scanner Instance (process) and run a scan with the following options. 26 | request :post, 'instances', { 27 | peplum: { 28 | objects: ['192.168.1.*'], 29 | max_workers: 5 30 | }, 31 | payload: { 32 | connect_scan: true, 33 | service_scan: true, 34 | default_script: true 35 | } 36 | } 37 | 38 | # The ID is used to represent that instance and allow us to manage it from here on out. 39 | instance_id = response_data['id'] 40 | 41 | while sleep( 1 ) 42 | request :get, "instances/#{instance_id}/info/progress" 43 | ap response_data 44 | 45 | # Continue looping while instance status is 'busy'. 46 | request :get, "instances/#{instance_id}" 47 | break if !response_data['busy'] 48 | end 49 | 50 | puts '*' * 88 51 | 52 | # Get the scan report. 53 | request :get, "instances/#{instance_id}/report.json" 54 | 55 | # Print out the report. 56 | puts JSON.pretty_generate( JSON.load( response_data['data'] ) ) 57 | 58 | # Shutdown the Instance. 59 | request :delete, "instances/#{instance_id}" 60 | -------------------------------------------------------------------------------- /examples/rest/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'tmpdir' 3 | require 'net/http' 4 | 5 | def response 6 | if @last_response['Content-Type'].include? 'json' 7 | data = JSON.load( @last_response.body ) 8 | else 9 | data = @last_response.body 10 | end 11 | { 12 | code: @last_response.code, 13 | data: data 14 | } 15 | end 16 | 17 | def response_data 18 | response[:data] 19 | end 20 | 21 | def request( method, resource = nil, parameters = nil ) 22 | uri = URI( "http://127.0.0.1:7331/#{resource}" ) 23 | 24 | Net::HTTP.start( uri.host, uri.port) do |http| 25 | case method 26 | when :get 27 | uri.query = URI.encode_www_form( parameters ) if parameters 28 | request = Net::HTTP::Get.new( uri ) 29 | 30 | when :post 31 | request = Net::HTTP::Post.new( uri ) 32 | request.body = parameters.to_json 33 | 34 | when :delete 35 | request = Net::HTTP::Delete.new( uri ) 36 | 37 | when :put 38 | request = Net::HTTP::Put.new( uri ) 39 | request.body = parameters.to_json 40 | end 41 | 42 | @last_response = http.request( request ) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /examples/rpc.rb: -------------------------------------------------------------------------------- 1 | require 'pp' 2 | require 'peplum/nmap' 3 | 4 | # Spawn an nmap Agent as a daemon. 5 | nmap_agent = Peplum::Nmap::Application.spawn( :agent, daemonize: true ) 6 | at_exit { nmap_agent.shutdown rescue nil } 7 | 8 | # Spawn and connect to an nmap Instance. 9 | nmap = Peplum::Nmap::Application.connect( nmap_agent.spawn ) 10 | # Don't forget this! 11 | at_exit { nmap.shutdown } 12 | 13 | # Run a distributed scan. 14 | nmap.run( 15 | peplum: { 16 | objects: ['192.168.1.*'], 17 | max_workers: 5 18 | }, 19 | payload: { 20 | connect_scan: true, 21 | service_scan: true, 22 | default_script: true 23 | } 24 | ) 25 | 26 | # Waiting to complete. 27 | while nmap.running? 28 | ap nmap.info.progress 29 | sleep 1 30 | end 31 | 32 | # Hooray! 33 | puts JSON.pretty_generate( nmap.generate_report.data ) 34 | -------------------------------------------------------------------------------- /lib/peplum/nmap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'peplum' 4 | 5 | module Peplum 6 | class Nmap 7 | 8 | require_relative "nmap/version" 9 | require_relative "nmap/payload" 10 | 11 | class Error < Peplum::Error; end 12 | 13 | class Application < Peplum::Application 14 | 15 | require_relative "nmap/services/info" 16 | instance_service_for :info, Services::Info 17 | 18 | require_relative "nmap/services/rest_proxy" 19 | rest_service_for :info, Services::RESTProxy 20 | 21 | def payload 22 | Payload 23 | end 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/peplum/nmap/payload.rb: -------------------------------------------------------------------------------- 1 | require 'nmap/command' 2 | require 'nmap/xml' 3 | require 'tmpdir' 4 | 5 | module Peplum 6 | class Nmap 7 | 8 | module Payload 9 | include Peplum::Application::Payload 10 | 11 | DEFAULT_OPTIONS = { 12 | 'output_normal' => '/dev/null', 13 | 'quiet' => true 14 | } 15 | 16 | PING_REPORT = "#{Dir.tmpdir}/nmap-ping-#{Process.pid}.xml" 17 | SCAN_REPORT = "#{Dir.tmpdir}/nmap-scan-#{Process.pid}.xml" 18 | 19 | at_exit do 20 | FileUtils.rm_f PING_REPORT 21 | FileUtils.rm_f SCAN_REPORT 22 | end 23 | 24 | def run( targets, options ) 25 | # Do it this way so we'll be able to have progress reports per scanned host. 26 | targets.map do |target| 27 | _run options.merge( targets: target, output_xml: SCAN_REPORT ) 28 | 29 | report = report_from_xml( SCAN_REPORT ) 30 | next if report.empty? 31 | 32 | Nmap::Application.master.info.update report 33 | report 34 | end.compact 35 | end 36 | 37 | def split( targets, chunks ) 38 | @hosts ||= self.live_hosts( targets ) 39 | @hosts.chunk( chunks ).reject(&:empty?) 40 | end 41 | 42 | private 43 | 44 | def live_hosts( targets ) 45 | _run targets: targets, 46 | ping: true, 47 | output_xml: PING_REPORT 48 | 49 | hosts = hosts_from_xml( PING_REPORT ) 50 | 51 | # Seed the progress data with the live hosts 52 | hosts.each { |h| Services::Info.progress_data[h] ||= {} } 53 | 54 | hosts 55 | end 56 | 57 | def set_default_options( nmap ) 58 | set_options( nmap, DEFAULT_OPTIONS ) 59 | end 60 | 61 | def set_options( nmap, options ) 62 | options.each do |k, v| 63 | nmap.send "#{k}=", v 64 | end 65 | end 66 | 67 | def _run( options = {}, &block ) 68 | ::Nmap::Command.run do |nmap| 69 | set_default_options nmap 70 | set_options nmap, options 71 | block.call nmap if block_given? 72 | end 73 | end 74 | 75 | def hosts_from_xml( xml ) 76 | hosts = [] 77 | ::Nmap::XML.open( xml ) do |xml| 78 | xml.each_host do |host| 79 | hosts << host.ip 80 | end 81 | end 82 | hosts 83 | end 84 | 85 | def report_from_xml( xml ) 86 | report_data = {} 87 | ::Nmap::XML.open( xml ) do |xml| 88 | xml.each_host do |host| 89 | report_data[host.ip] = host_to_hash( host ) 90 | 91 | report_data[host.ip]['ports'] = {} 92 | host.each_port do |port| 93 | report_data[host.ip]['ports'][port.number] = port_to_hash( port ) 94 | end 95 | end 96 | end 97 | report_data 98 | end 99 | 100 | def host_to_hash( host ) 101 | h = {} 102 | %w(start_time end_time status addresses mac vendor ipv4 ipv6 hostname hostnames os uptime).each do |k| 103 | v = host.send( k ) 104 | next if !v 105 | 106 | if v.is_a? Array 107 | h[k] = v.map(&:to_s) 108 | else 109 | h[k] = v.to_s 110 | end 111 | end 112 | 113 | if host.host_script 114 | h['scripts'] = {} 115 | host.host_script.scripts.each do |name, script| 116 | h['scripts'][name] = { 117 | output: script.output, 118 | data: script.data 119 | } 120 | end 121 | end 122 | 123 | h 124 | end 125 | 126 | def port_to_hash( port ) 127 | h = {} 128 | 129 | %w(protocol state reason reason_ttl).each do |k| 130 | h[k] = port.send( k ) 131 | end 132 | h['service'] = port.service.to_s 133 | 134 | h['scripts'] ||= {} 135 | port.scripts.each do |name, script| 136 | h['scripts'][name] = { 137 | output: script.output, 138 | data: script.data 139 | } 140 | end 141 | 142 | h 143 | end 144 | 145 | extend self 146 | 147 | end 148 | 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/peplum/nmap/services/info.rb: -------------------------------------------------------------------------------- 1 | module Peplum 2 | class Nmap 3 | module Services 4 | 5 | class Info 6 | 7 | class <