A simple ruby script that takes a single file or a directory with several NMAP xml files and creates a csv or a pdf with a list mapping open ports to a list of IP addresses. If you want to use the PDF feature it needs a gem , so you need to install prawn, otherwise it should work with a default ruby installation.

sudo gem install prawn

HELP:

./nmaports.rb --help 

HELP for Nmaport
---------------------

      --help, -h
        Well I guess you know what this is for (To obtain this Help).

      --dir, -d [directory_name]
         The root path for the nmap files.

      --pattern, -p
         The pattern to use to detect file (i.e *client*).

      --file, -f [file_name] 
        If we only want one file.

      --output, -o
        The output file name.

SAMPLE:

> ./nmapports -d nmaps -o  openports.pdf

CODE:

#!/usr/bin/env ruby -wKU
# Author : FreedomCoder ( Matias Pablo Brutti )
# Email: matiasbrutti ---- gmail ---- com
# Created on : February 9, 2009
# License: MIT
# Description : NMAP output parser that creates a table of  Ports --> IPs. 
# This is helpful to create a list of open ports and their corresponding IP addresses. 
 
 
require 'rubygems'
require 'getoptlong'
require 'rexml/document'
 
@nmap_dir, @nmap_file, @pattern, @output, @type = nil
 
opts = GetoptLong.new(
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
['--dir','-d', GetoptLong::REQUIRED_ARGUMENT ],
['--file','-f', GetoptLong::REQUIRED_ARGUMENT ],
['--pattern','-p', GetoptLong::REQUIRED_ARGUMENT ],
['--output','-o', GetoptLong::REQUIRED_ARGUMENT ],
['--type','-t', GetoptLong::REQUIRED_ARGUMENT ]
)
 
opts.each do |opt, arg|
  case opt
    when '--help':
      # BEGIN OF HELP
      puts "\nHELP for Nmaport\n---------------------\n
      --help, -h
      \tWell I guess you know what this is for (To obtain this Help).\n
      --dir, -d [directory_name]
      \t The root path for the nmap files.\n
      --pattern, -p
      \t The pattern to use to detect file (i.e *client*).\n
      --file, -f [file_name] 
      \tIf we only want one file.\n
      --output, -o
      \tThe output file name.
      Copyright 2009 - FreedomCoder\n"
      #END OF HELP
      exit(0)
    when '--dir':
      if File.exists?(arg)
        @nmap_dir = arg
      else
        puts "Directory not found"
      end
 
    when '--file':
      if File.exists?(arg)
        @nmap_file = arg
      else
        puts "File not found"
      end
    when '--pattern':
      @pattern = arg
    when '--output':
      @output = arg
      @type = @output.split(".")[1].upcase unless @type
    when '--type':
      if arg =~ /PDF|pdf|CSV|csv/
        @type = arg.upcase
      else
        puts "unrecognized file type. bye!"
        exit(0)
      end
    else
      puts "Unknown command. Please try again"
     exit(0)
  end
end
 
 
# method to read files from directoy ---------------------------------------------------------------
 
def get_files(dir,name)
  files = Dir["#{dir}/**/#{name || "*"}.xml"]
end
 
def dir_open_ports(dir)
  arr = []
  dir.each do |doc| 
    arr += open_ports(read_xml(doc))
  end
  arr.uniq.sort
end
 
def dir_get_ips(dir, ports)
  final_list = {}
  dir.each do |doc|
    puts "Working on #{doc}\n"
    final_list.merge!(get_ips(read_xml(doc),ports)) { |k,o,n| final_list[k] =  (o + n).sort.uniq }
  end
  final_list  
end
 
# methods to parse XML Nmap output -----------------------------------------------------------------
 
def read_xml(xml)
  doc = REXML::Document.new(File.read(xml)).root
end
 
def open_ports(doc)
  out = []
  doc.elements.each('host') do |h|
    h.elements.each('ports/port') do |p|
      if p.elements['state'].attributes['state'] == "open" 
        out << "#{p.attributes['portid']}/#{p.attributes['protocol']}"
      end
    end
  end
  out.uniq.sort
end
 
def open?(host,port)
  host.elements.each('ports/port') do |p|
    if p.attributes['portid'] == port.split("/")[0] && p.elements['state'].attributes['state'] == "open"
      return true
    end
  end
  false
end
 
def get_ips(doc,ports)
  open_list = {}
  ports.each do |port|
    a = []
    doc.elements.each('host') do |h|
      a << h.elements['address'].attributes['addr'] if open?(h,port)
    end
    open_list[port] = a
  end
  open_list
end
 
#methods to output the data ------------------------------------------------------------------------
 
def create_output(list,name,type)
  case type
    when "PDF":
      create_pdf(list,name)
      puts "Data written to file #{@output || "output.pdf"}"
    when "CSV":
      create_csv(list,name)
      puts "Data written to file #{@output || "output.csv"}"
    else
      puts "You did not specified an output type using CSV"
      create_csv(list,name)
      puts "Data written to file #{@output || "output.csv"}"
  end
end
 
def create_csv(list,name=nil)
  out = File.new(name || "output.csv", "w")
  out << "PORT,IP Adressess\n"
  list.each do |k,v|
    out << "#{k},\"#{(v.map { |k| k + "\n" }.to_s).strip}\" \n"
  end
end
 
def create_pdf(list,name=nil)
  require 'prawn'
  require 'prawn/layout'
  Prawn::Document.generate(name || "output.pdf") do 
 
    data = []
    list.each do |k,v|
      data << [k,(v.map { |k| k + "\n" }.to_s).strip]
    end
 
    table data, 
      :position => :center, 
      :headers => ["Port", "IP Adresses"],
      :header_color => "0046f9",
      :row_colors => :pdf_writer, #["ffffff","ffff00"],
      :font_size => 10,
      :vertical_padding => 2,
      :horizontal_padding => 5
  end
end
 
def show_data(list)
  puts "PORT\t  IP Adressess\n"
  list.each do |k,v|
    puts "#{k}\t#{(v.map { |k| "        " + k + "\n" }.to_s).strip} \n"
    puts "--------------------------"
  end
end
 
# Script -------------------------------------------------------------------------------------------
puts "Let's work ..."
lista = {}
 
if @nmap_dir
  dir_list = get_files(@nmap_dir,@patern)
  ports = dir_open_ports(dir_list)
  lista = dir_get_ips(dir_list, ports)
end
 
if @nmap_file
  xml_file = read_xml(@nmap_file)
  ports = open_ports(xml_file)
  if lista.empty? 
    lista = get_ips(xml_file,ports)
  else
    lista.merge!(get_ips(xml_file,ports)) do |k,o,n| 
      lista[k] =  (o + n).sort.uniq
    end
  end
end
 
show_data(lista)
create_output(lista,@output,@type)
puts "Bye"

Enjoy.