#!/usr/bin/env ruby # $Id$ # $URL$ # parse nbe files and group IPs of matching vulnerabilities require 'getoptlong' require 'rexml/streamlistener' require 'rexml/document' @vulns = Hash.new Vuln = Struct.new(:ip, :fullip, :descr, :weight) Weight= { 'Security Note' => 0, 'Security Warning' => 1, 'Security Hole' => 2, 'NOTE' => 0, 'INFO' => 1, 'REPORT' => 2 } module Color AnsiAttributes = { 'clear' => 0, 'reset' => 0, 'bold' => 1, 'dark' => 2, 'underline' => 4, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'concealed' => 8, 'black' => 30, 'on_black' => 40, 'red' => 31, 'on_red' => 41, 'green' => 32, 'on_green' => 42, 'yellow' => 33, 'on_yellow' => 43, 'blue' => 34, 'on_blue' => 44, 'magenta' => 35, 'on_magenta' => 45, 'cyan' => 36, 'on_cyan' => 46, 'white' => 37, 'on_white' => 47 } # # Return a string with ANSI codes substituted. Derived from code # written by The FaerieMUD Consortium. # def self.ansi(*attrs) attr = attrs.collect {|a| AnsiAttributes[a] ? AnsiAttributes[a] : nil}.compact.join(';') attr = "\e[%sm" % attr if ! attr.empty? return attr end end class XMLListener Vuln = Struct.new(:ip, :fullip, :descr, :weight) def initialize() @curpath = Array.new @curpathstr = "" @curhost = "" @vulns = Hash.new end def clearhost @curhost = "" end def clearreport @curport = "" @curseverity = "" @curpluginID = "" @curpluginName = "" @curdata = "" end def gethash return @vulns end def tag_start(name, attrs) @curpath.push name @curpathstr = @curpath.join('/') # puts @curpathstr if @curpathstr == 'NessusClientData/Report/ReportHost' clearhost end end def tag_end(name) @curpath.pop @curpathstr = @curpath.join('/') if @curpathstr == 'NessusClientData/Report/ReportHost' # nu is een report volledig, dus toevoegen aan standaard meuk ofzo # nog even uitzoeken /hoe/ if ! @curseverity.nil? and @curseverity.to_i > 0 if ! @vulns[@curpluginID].nil? @vulns[@curpluginID].push Vuln.new(@curhost, "#{@curhost} : #{@curport}", @curdata.gsub(/\\n/, "\n").gsub(/\\r/, "\r").gsub(/\\\\/, "\\"), @curseverity.to_i - 1) else @vulns[@curpluginID] = [ Vuln.new(@curhost, "#{@curhost} : #{@curport}", @curdata.gsub(/\\n/, "\n").gsub(/\\r/, "\r").gsub(/\\\\/, "\\"), @curseverity.to_i - 1) ] end end clearreport end end def text(text) if @curpathstr == 'NessusClientData/Report/ReportHost/HostName' @curhost = text end if @curpathstr == 'NessusClientData/Report/ReportHost/ReportItem/port' @curport = text end if @curpathstr == 'NessusClientData/Report/ReportHost/ReportItem/severity' @curseverity = text # is hier precies 1 hoger dan ik zelf bedacht had # severity 0 = boeie # severity 1 = note # severity 2 = warning # severity 3 = hole end if @curpathstr == 'NessusClientData/Report/ReportHost/ReportItem/pluginID' @curpluginID = text end if @curpathstr == 'NessusClientData/Report/ReportHost/ReportItem/pluginName' @curpluginName = text end if @curpathstr == 'NessusClientData/Report/ReportHost/ReportItem/data' @curdata = text end end end def usage puts < -i ids is a comma seperated list of NessusIDs to be displayed (default: all) -f display full list of descriptions (default: only the first) -C do not display colors -s give a summary of the IDs found -t use NessusId 21643 to generate a table of SSL-weaknesses (tab delimited) EOT exit end def parse_options @options = {} begin opts = GetoptLong.new( [ "-i", GetoptLong::REQUIRED_ARGUMENT ], [ "-f", GetoptLong::NO_ARGUMENT ], [ "-h", "--help", GetoptLong::NO_ARGUMENT ], [ "-C", GetoptLong::NO_ARGUMENT ], [ "-s", GetoptLong::NO_ARGUMENT ], [ "-t", GetoptLong::NO_ARGUMENT ] ) opts.quiet=true opts.each do |opt, arg| @options[opt] = arg end if @options["-h"] usage end rescue GetoptLong::InvalidOption print "#{$!}\n" usage end # default values if @options["-f"].nil? @options["-f"] = false end if @options["-C"].nil? @options["-C"] = false end if @options["-s"].nil? @options["-s"] = false end return @options end def colorize(string, color) if @options["-C"] string else "#{Color.ansi(color)}#{string}#{Color.ansi("clear")}" end end def display(nessusid) next if nessusid.nil? puts "==========================================" if @vulns[nessusid].nil? puts "NessusID: #{nessusid} not found" return end puts "NessusID: #{nessusid} IPs: #{@vulns[nessusid].collect{|i| i[:ip]}.join(" ")}" if ! @options["-f"] case @vulns[nessusid][0][:weight] when 2 then puts colorize("Severity: Security Hole", "on_red") when 1 then puts colorize("Severity: Security Warning", "on_yellow") when 0 then puts colorize("Severity: Security Note", "on_cyan") end puts "Full IPs:\n#{@vulns[nessusid].collect{|i| i[:fullip]}.join("\n")}" puts "First description (these can differ per IP!):" puts "#{@vulns[nessusid][0][:descr]}" else @vulns[nessusid].collect{|vuln| case vuln[:weight] when 2 then puts colorize("Severity: Security Hole #{vuln[:fullip]}", "on_red") when 1 then puts colorize("Severity: Security Warning #{vuln[:fullip]}", "on_yellow") when 0 then puts colorize("Severity: Security Note #{vuln[:fullip]}", "on_cyan") end puts "Description:" puts "#{vuln[:descr]}" } end puts end def summary(weight) case weight when 2 then puts colorize("Severity: Security Hole", "on_red") when 1 then puts colorize("Severity: Security Warning", "on_yellow") when 0 then puts colorize("Severity: Security Note", "on_cyan") end @vulns.keys.sort.each{|nessusid| if @vulns[nessusid][0][:weight] == weight puts "NessusID: #{nessusid} IPs: #{@vulns[nessusid].collect{|i| i[:ip]}.join(" ")}" end } puts end def ssltable sslID="21643" if @vulns[sslID].nil? puts "No SSL-ports found" return end puts "IP port EXP LOW MD5 SSLv2" @vulns[sslID].collect{|vuln| port = vuln[:fullip].sub(/.*\(/, "").sub(/\/.*/, "") exp = vuln[:descr].match(/Enc=[^\(]*\(40\)/) low = vuln[:descr].match(/Enc=[^\(]*\(56\)/) md5 = vuln[:descr].match(/Mac=MD5/) sslv2 = vuln[:descr].match(/SSLv2/) puts "#{vuln[:ip]} #{port} #{exp ? "X" : "-"} #{low ? "X" : "-"} #{md5 ? "X" : "-"} #{sslv2 ? "X" : "-"}" } end def highestweight(nessusid) highest = 0 @vulns[nessusid].each{|i| highest = i[:weight] if i[:weight] > highest } highest end def readnbe(inputfile) File.open(inputfile).each_line{|line| sl = Array.new sl = line.split('|', 7) if sl[0] == "results" next if sl[4].nil? if ! @vulns[sl[4]].nil? @vulns[sl[4]].push Vuln.new(sl[2], "#{sl[2]} : #{sl[3]}", sl[6].gsub(/\\n/, "\n").gsub(/\\r/, "\r").gsub(/\\\\/, "\\"), Weight[sl[5]]) else @vulns[sl[4]] = [ Vuln.new(sl[2], "#{sl[2]} : #{sl[3]}", sl[6].gsub(/\\n/, "\n").gsub(/\\r/, "\r").gsub(/\\\\/, "\\"), Weight[sl[5]]) ] end end } end def readnsr(inputfile) File.open(inputfile).each_line{|line| sl = Array.new sl = line.split('|') next if sl[3].nil? if ! @vulns[sl[2]].nil? @vulns[sl[2]].push Vuln.new(sl[0], "#{sl[0]} : #{sl[1]}", sl[4].gsub(/;;/, "\n"), Weight[sl[3]]) else @vulns[sl[2]] = [ Vuln.new(sl[0], "#{sl[0]} : #{sl[1]}", sl[4].gsub(/;/, "\n"), Weight[sl[3]]) ] end } end def readnessus(inputfile) list = XMLListener.new source = File.new(inputfile) REXML::Document.parse_stream(source, list) @vulns = list.gethash end parse_options if ARGV[0].nil? or ARGV[0].empty? or ! FileTest.exists?(ARGV[0]) usage end # de regels in .nbe bestanden beginnen met 'timestamps|' of 'results|' # de regels in .nsr bestanden beginnen met een IP-adres # de regels in .nessus bestanden beginnen met een '<' (de eerste regel is: ) line = File.open(ARGV[0]).readline if line.match(/^(timestamps|results)\|/) readnbe(ARGV[0]) elsif line.match(/^\d+\.\d+\.\d+\.\d+/) readnsr(ARGV[0]) elsif line.match(/^/) readnessus(ARGV[0]) end if @options["-s"] summary(Weight['Security Hole']) summary(Weight['Security Warning']) summary(Weight['Security Note']) exit end if @options["-t"] ssltable exit end if @options["-i"] @options["-i"].split(",").each{|key| display(key) } else @vulns.keys.sort.each{|key| display(key) if highestweight(key) == Weight['Security Hole'] } @vulns.keys.sort.each{|key| display(key) if highestweight(key) == Weight['Security Warning'] } @vulns.keys.sort.each{|key| display(key) if highestweight(key) == Weight['Security Note'] } end