Module: NmapAutoAnalyzer

Defined in:
lib/nmap_auto_analyzer.rb,
lib/nmap_auto_analyzer/version.rb

Constant Summary collapse

VERSION =
"0.0.1"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Attribute Details

#executeObject

Returns the value of attribute execute.



5
6
7
# File 'lib/nmap_auto_analyzer.rb', line 5

def execute
  @execute
end

#optionsObject

Returns the value of attribute options.



5
6
7
# File 'lib/nmap_auto_analyzer.rb', line 5

def options
  @options
end

#parsed_hostsObject

Returns the value of attribute parsed_hosts.



5
6
7
# File 'lib/nmap_auto_analyzer.rb', line 5

def parsed_hosts
  @parsed_hosts
end

#scan_filesObject

Returns the value of attribute scan_files.



5
6
7
# File 'lib/nmap_auto_analyzer.rb', line 5

def scan_files
  @scan_files
end

#scanned_filesObject

Returns the value of attribute scanned_files.



5
6
7
# File 'lib/nmap_auto_analyzer.rb', line 5

def scanned_files
  @scanned_files
end

#valid_entriesObject

Returns the value of attribute valid_entries.



5
6
7
# File 'lib/nmap_auto_analyzer.rb', line 5

def valid_entries
  @valid_entries
end

Class Method Details

.excel_reportObject



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/nmap_auto_analyzer.rb', line 317

def self.excel_report
  begin
    require 'rubyXL'
  rescue LoadError
    puts "Couldn't load rubyXL, try gem install rubyXL"
    exit
  end
  
  workbook = RubyXL::Workbook.new
  sheet = workbook.worksheets[0]
  
  sheet.add_cell(0,0,"IP Address")
  sheet.add_cell(0,1,"TCP Ports")
  sheet.add_cell(0,2,"UDP Ports")
  curr_row = 1
  sorted_hosts = @parsed_hosts.sort_by {|address,find| address.split('.').map{ |digits| digits.to_i}}
  sorted_hosts.each do |entry|
    host, ports = entry[0], entry[1]
    next if ports.length == 0
    tcp_ports = Array.new
    udp_ports = Array.new
    ports.each do |port,data|
      portnum, protocol = port.split(' - ')
      if protocol == 'TCP'
        tcp_ports << portnum
      elsif protocol == 'UDP'
        udp_ports << portnum
      end

    end
    sheet.add_cell(curr_row,0,host)
    sheet.add_cell(curr_row,1,tcp_ports.join(', '))
    sheet.add_cell(curr_row,2,udp_ports.join(', '))
    curr_row = curr_row + 1
  end

  workbook.write(@excel_report_file_name)
end

.execute(commandlineopts) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/nmap_auto_analyzer.rb', line 7

def self.execute(commandlineopts)

  @options = commandlineopts
  
  require 'rubygems'
  require 'logger'
  
  @base_dir = @options.report_directory
  @scan_dir = @options.scan_directory
  if !File.exists?(@base_dir)
    Dir.mkdirs(@base_dir)
  end

  @log = Logger.new(@base_dir + '/nmap-analyzer-log')
  @log.level = Logger::DEBUG
  @log.debug("Log created at " + Time.now.to_s)
  @log.debug("Scan type is : #{@options.scan_type}")
  @log.debug("Directory being scanned is : #{@options.scan_directory}") if @options.scan_type == :directory 
  @log.debug("File being scanned is : #{@options.scan_file}") if @options.scan_type == :file 
  
  @report_file_name = @base_dir + '/' + @options.report_file
  @report_file = File.new(@report_file_name + '.txt','w+')
  @html_report_file = File.new(@report_file_name + '.html','w+')
  @excel_report_file_name = @report_file_name + '.xlsx'
  @log.info("New report file created #{@report_file_name}")
  run
end

.html_reportObject

Generates an HTML report with the results



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/nmap_auto_analyzer.rb', line 300

def self.html_report
  begin
    require 'kramdown'
  rescue LoadError
    puts "Couldn't load kramdown, try gem install kramdown"
    exit
  end
  
  
  base_report = File.open(@report_file_name + '.txt','r').read
  
  report = Kramdown::Document.new(base_report)
  
    
  @html_report_file.puts report.to_html
end

.parse_filesObject

Parses the nmap xml files and populates the arrays needed by the report



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/nmap_auto_analyzer.rb', line 78

def self.parse_files
  #Hash to put the information for each host into
  @parsed_hosts = Hash.new
  @scanned_files = Hash.new
  @closed_ports = Array.new
  #Ports is an array to contain a list of the unique ports encountered for later feeding to Nessus
  @ports = Array.new
  #port_hash is a hash to contain a list of ports and what hosts have them open
  @port_hash = Hash.new
  @traceroute_hash = Hash.new
  @os_hash = Hash.new
  @web_headers_hash = Hash.new
  @scan_files.each do |file|
    begin
      parser = Nmap::Parser.parsefile(file)
    rescue IOError => e
      @log.warn("Warning couldn't parse file #{file}")
      puts "Couldn't parse file #{file}, check to make sure it wasn't critical!!"
      next
    rescue REXML::ParseException
      @log.warn("Warning, couldn't parse file #{file} due to an XML parse error")
      puts "Warning, couldn't parse file #{file} due to an XML parse error"
      next
    end
    @scanned_files[file] = Hash.new unless @scanned_files[file]
    @scanned_files[file][:scan_args] = parser.session.scan_args if parser.session.scan_args
    @scanned_files[file][:scan_time] = parser.session.scan_time if parser.session.scan_time
    parser.hosts("up") do |host|
      #TODO: we should add UDP here too, but watch for no-response otherwise we'll get false positive centraled.
      next if @options.ignore_chatty && host.tcp_ports("open").length > 900
      @parsed_hosts[host.addr] = Hash.new unless @parsed_hosts[host.addr]
      host.extraports.each do |portstate|
        if portstate.state == "closed" && portstate.count > 1
          @closed_ports << host.addr
        end
      end

      #Add Traceroute information and grab the last hop before the host
      #It's either the last hop or the one before it
      #So it looks to me that nmaps traceroute isn't quite right for this
      #produces different results to traceroute...
      #if host.traceroute
      #  @log.debug("host address is " + host.addr +  "Last traceroute is" + host.traceroute.hops[-1].addr)
      #  if host.traceroute.hops[-1].addr != host.addr || host.traceroute.hops.length == 1
      #    last_hop = host.traceroute.hops[-1].addr.to_s
      #  else
      #    last_hop = host.traceroute.hops[-2].addr.to_s
      #  end
      #  @traceroute_hash[host.addr] = last_hop
      #end

      #Add OS guess information
      if host.os.name
        @os_hash[host.addr] = host.os.name + ', ' + host.os.name_accuracy.to_s
      end


      host.tcp_ports("open") do |port|
        #Add the port to the ports array
        @ports << port.num.to_s
        #Add the port to the port hash
        if @port_hash[port.num.to_s + '-TCP']
          @port_hash[port.num.to_s + '-TCP'] << host.addr
        else
          @port_hash[port.num.to_s + '-TCP'] = Array.new
          @port_hash[port.num.to_s + '-TCP'] << host.addr
        end
        @parsed_hosts[host.addr][port.num.to_s + ' - TCP'] = Hash.new 
        @parsed_hosts[host.addr][port.num.to_s + ' - TCP'][:service] = port.service.name if port.service.name
        @parsed_hosts[host.addr][port.num.to_s + ' - TCP'][:reason] = port.reason if port.reason
        @parsed_hosts[host.addr][port.num.to_s + ' - TCP'][:product] = port.service.product if port.service.product
        if host.tcp_script(port.num.to_s, 'http-methods')
          @web_headers_hash[host.addr + ':' + port.num.to_s] = host.tcp_script(port.num.to_s, 'http-methods').output.split("\n")[0]
        end
      end
      


      host.udp_ports("open") do |port|
        next if port.reason == "no-response"
        #Add the port to the ports array
        @ports << port.num.to_s
        #Add the port to the port hash
        if @port_hash[port.num.to_s + '-UDP']
          @port_hash[port.num.to_s + '-UDP'] << host.addr
        else
          @port_hash[port.num.to_s + '-UDP'] = Array.new
          @port_hash[port.num.to_s + '-UDP'] << host.addr
        end
        @parsed_hosts[host.addr][port.num.to_s + ' - UDP'] = Hash.new
        @parsed_hosts[host.addr][port.num.to_s + ' - UDP'][:service] = port.service.name if port.service.name
        @parsed_hosts[host.addr][port.num.to_s + ' - UDP'][:reason] = port.reason if port.reason
        @parsed_hosts[host.addr][port.num.to_s + ' - UDP'][:product] = port.service.product if port.service.product  	
      end  
    end
  end
#Once we're done with the files clean up the ports array
@ports.uniq!
end

.reportObject

Generates a kramdown compatible report that we can use to generate an HTML report



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/nmap_auto_analyzer.rb', line 179

def self.report
  @report_file.puts "NMAP AUTO analysis"
  @report_file.puts "===================\n\n"
  @report_file.puts "Unique ports open"
  @report_file.puts "-------------------\n\n"
  @report_file.puts @ports.join(', ')
  @report_file.puts "\n\n"
  @report_file.puts "NMAP Host Analysis"
  @report_file.puts "-------------------"
  @report_file.puts ""
  @report_file.puts "Active Host List"
  @report_file.puts "---------"
  active_ipaddresses = Array.new
  inactive_ipaddresses = Array.new
  @parsed_hosts.each do |entry|
    host, ports = entry[0], entry[1]
    if ports.length > 0
      active_ipaddresses << host
    else
      inactive_ipaddresses << host
    end
  end
 @report_file.puts active_ipaddresses.uniq.join(', ')
  @report_file.puts ""
  @report_file.puts ""
  @report_file.puts "Inactive Host List"
  @report_file.puts "---------"
  @report_file.puts inactive_ipaddresses.uniq.join(', ')
  @report_file.puts ""
  @report_file.puts ""
  if @traceroute_hash.length > 0
    @report_file.puts "Traceroute Information"
    @report_file.puts "---------"
    @report_file.puts "Target Address, Last Hop"
    @traceroute_hash.each do |addr, last_hop|
      @report_file.puts addr + ", " + last_hop
    end
    @report_file.puts ""
    @report_file.puts ""
  end
  if @os_hash.length > 0
    @report_file.puts "Operating System Information"
    @report_file.puts "---------"
    @report_file.puts "Target Address, OS Guess, OS Accuracy"
    @os_hash.each do |addr, os|
      @report_file.puts addr + ", " + os
    end
    @report_file.puts ""
    @report_file.puts ""
  end

  if @web_headers_hash.length > 0
    @report_file.puts "Operating System Information"
    @report_file.puts "---------"
    @report_file.puts "Target Web Server, Supported Methods"
    @web_headers_hash.each do |addr, methods|
      @report_file.puts addr + ", " + methods
    end
    @report_file.puts ""
    @report_file.puts ""
  end

  #sorted_hosts = @parsed_hosts.sort {|a,b| b[1].length <=> a[1].length}
  sorted_hosts = @parsed_hosts.sort_by {|address,find| address.split('.').map{ |digits| digits.to_i}}

  sorted_hosts.each do |entry|
    host, ports = entry[0], entry[1]
    #This omits any hosts that were deemed up but had no open ports
    #TODO: Make this an option in reporting (verbose against concise)
    next if ports.length == 0
    @report_file.puts "Host address: #{host} was detected as up by nmap"
    @report_file.puts "------------------------------------------------"
    @report_file.puts ""
    if ports.length == 0
      @report_file.puts "No Ports detected as open on this host"
    end
    ports.each do |port, data|
      
      @report_file.print "Port #{port} is open "
      @report_file.print ", Service name is #{data[:service]}" if data[:service]
      @report_file.print ", Service Product name is #{data[:product]}" if data[:product]
      @report_file.print ", Up reason is #{data[:reason]}" if data[:reason]
      @report_file.puts ""
      
    end
    @report_file.puts ""
    @report_file.puts "-------------------"
    @report_file.puts ""
  end

  @report_file.puts "\n\nReport of hosts with a given port open"
  @report_file.puts "--------------------\n\n"
  @port_hash.each do |port,hosts|
    @report_file.puts port + ' - ' + hosts.uniq.length.to_s + ' hosts have this port open'
    @report_file.puts "-----------------"
    @report_file.puts hosts.uniq.join(', ')
    @report_file.puts "\n\n"
  end

  @report_file.puts "\n\nActive Hosts with Closed Ports"
  @report_file.puts "--------------------\n\n"
  @report_file.puts @closed_ports.uniq.join(', ')
  @report_file.puts "--------------------\n\n"
  active_ipaddresses.each do |add|
    result = "n"
    result = "y" if @closed_ports.include?(add)
    @report_file.puts add + ', ' + result
  end

  #TODO: Make this an option in terms of reporting volume
  @report_file.puts "\n\nNMAP runs analysed"
  @scanned_files.each do |file, data|
    @report_file.puts "\n-------------------"
    @report_file.puts file
    @report_file.puts "Scan arguments were #{data[:scan_args]}"
    @report_file.puts "Scan Time was #{data[:scan_time]}"
  end
  @report_file.close
end

.runObject

Sets up the process for scanning the xml files and calls the individual methods depending on the scan type



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/nmap_auto_analyzer.rb', line 37

def self.run
  
  case @options.scan_type
  #Directory Mode
  when :directory
    @valid_entries = Array.new
    @valid_entries << ''
    scan_dirs
    parse_files
    report
    excel_report
    if @options.html_report
      html_report
    end
  #File Mode     
  when :file
    @scan_files = Array.new
    @scan_files << @options.scan_file
    parse_files
    report
    excel_report
    if @options.html_report
      html_report
    end
  end
end

.scan_dirsObject

Adds all the xml files in the directory being scanned to the scan_files array



65
66
67
68
69
70
71
72
73
74
# File 'lib/nmap_auto_analyzer.rb', line 65

def self.scan_dirs
  @scan_files = Array.new
  @valid_entries.each do |dir|
    Dir.entries(@scan_dir + dir).each do |scan|
      if scan =~ /xml$/
        @scan_files << @scan_dir + dir + '/' + scan
      end
    end
  end
end