diff --git a/nessus-group/nessus-group.rb b/nessus-group/nessus-group.rb new file mode 100755 index 0000000..5911a6d --- /dev/null +++ b/nessus-group/nessus-group.rb @@ -0,0 +1,352 @@ +#!/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