tissynbe.py
As discussed in another post, tissynbe.py is a script I developed in Python that helped me analyze mountains of Nessus results quickly. The script cleans up the data and can insert it into a database or output to a CSV file. One other thing it does is split up the descriptions from the solutions (as best as possible).
If you encounter any errors with the script, it should send the offending lines to stdout. Simply copy/paste the output and email it to me (sanitized of course) and I will update the script. Of course, you can do this modification yourself as well, by adding the plugin to FIX, but I’d like to keep track of what plugins are causing trouble.
You can also download tissynbe.py directly. For database schema, see nessusdb.sql. I hope you find this tool useful and I encourage comments and suggestions!
Dependencies:
- Python 2.5 (to import __future__ module’s with_statement)
- Python MySQLdb (On Debian-based distros: apt-get install python-mysqldb)
#!/usr/bin/env python
from __future__ import with_statement
from time import strftime, strptime
import re
import sys
# tissynbe.py
# Copyright (C) 2008 Marcin Wielgoszewski (tssci-security.com)
#
# Thanks to the following people for their contributions:
# Romain Gaucher (rgaucher.info)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
__author__ = 'Marcin Wielgoszewski'
__version__ = '1.4a'
DB_HOST = 'hostname'
DB_UNAME = 'username'
DB_PASSWD = 'password'
"""Compiled regular expression objects.
These are the compiled regex's we want to keep around for performing string
substitution.
/(10287|10386|10761|10863|10919|19506)$/d
"""
DOTS = re.compile('( \.|\.\. )')
FIX = re.compile('(10180|10287|10330|10386|10662|10761|10863|10919|'
+'11011|11033|11153|11936|12053|12245|12634|14773|'
+'17975|18261|18528|19506|22964|'
+'11040|11822|11865|14674)\\|(1|2|3)')
GMT = re.compile('GMT(\!|\.)')
PIPE = re.compile('[ ]*\\|[ ]*')
SOLUTION = re.compile('(Solution:|Risk factor:|CVSS|Plugin output:|See also:|'
+'CVE:|BID:)')
SYNDESC = re.compile('( *Synopsis: *| *Description:)')
REPLACEMENTS = [
(' :',':'),
('the the','the'),
(' interfer ',' interfere '),
('Security Note','1'),
('Security Warning','2'),
('Security Hole','3'),
('\\\\','\\'),
('10862|3|','10862|3|The SQL Server has a common password for one or '
+'more accounts. These accounts may be used to gain access to the '
+'records in the database or even allow remote command execution.|'),
('21725|3|','21725|3|The remote host has an out-dated version of the '
+'Symantec Corporate virus signatures, or Symantec AntiVirus '
+'Corporate is not running.|'),
('22035|2|','22035|2|The version of Adobe Acrobat installed on the '
+'remote host is earlier than 6.0.5 and is reportedly affected by a '
+'buffer overflow that may be triggered when distilling a specially-'
+'crafted file to PDF.|'),
]
def clean_nbe(data):
"""Perform string replacements and substitutions on Nessus data.
This function performs much needed cleanup and does some prettifying of
Nessus results information. It removes double spaces, adds descriptions to
plugins missing them, and splits the plugin synopsis and description from
the vulnerability solutions. The order in which operations appear is
semi-important, so don't try to change them around.
"""
data = data.replace('\\n', ' ')
data = ' '.join(data.split())
for i, j in REPLACEMENTS:
data = data.replace(i, j)
data = DOTS.sub('.', data)
data = SYNDESC.sub('', data)
data = GMT.sub('GMT\\1|Renew the SSL certificate for the remote server.',
data, count=1)
data = SOLUTION.sub('|\\1', data, count=1)
data = PIPE.sub('|', data)
data = FIX.sub('\\1|\\2|', data)
data = data.rstrip(' ')
return data
def parse_nbe(nbe):
"""Open an nbe file, parse, then split into fields.
This code opens our input file we specified gracefully. It then begins to
process our data by calling clean_nbe() and then finally splits each line
on the pipe-delimiter. If a line has less fields than required, it will
print the line to stdout. Copy stdout to a file and send to
tissynbe _at_ tssci-security.com. I'll update the script to account for
these errors in processing.
"""
with open(nbe, 'rU') as file:
print """Processing""", nbe + """..."""
results = []
timestamps = []
problems = []
for line in file:
# Create a nested results list
if line.startswith("results"):
line = line.rstrip()
line = clean_nbe(line)
line = line.split('|',7)
if len(line) > 7:
results.append(line[1:])
elif len(line) > 4 and len(line) < 8:
problems.append(line)
# Create a nested timestamps list
elif line.startswith("timestamps"):
line = line.rstrip('|\n')
line = line.split('|')
line[4] = strftime("%Y-%m-%d %H:%M:%S", strptime(line[4]))
timestamps.append(line[2:])
if problems:
print """The following lines had problems:"""
for line in problems:
line = '|'.join(line)
print line.replace('\n',' ')
return results, timestamps
def insert_nbe(results,timestamps,database):
"""Insert parsed Nessus data into MySQL database.
This block of code will insert our processed Nessus data into the MySQL
database specified with the -d option on the command line. Before doing
so, ensure you have the proper database schema. After doing our SQL
INSERTs, the number of rows inserted into each table is printed for
reference. For database schema information see
http://www.tssci-security.com/upload/tissynbe_py/nessusdb.sql
"""
import MySQLdb
print """Executing SQL INSERT..."""
try:
db = MySQLdb.connect(DB_HOST,DB_UNAME,DB_PASSWD,database)
except MySQLdb.Error, e:
print """Error %d: %s""" % (e.args[0], e.args[1])
sys.exit (1)
c = db.cursor()
results_rows = 0
timestamps_rows = 0
while results:
small_results, results = results[:100], results[100:]
c.executemany("""INSERT INTO results
(domain, host, service, scriptid, riskval, msg1, msg2)
VALUES (%s, %s, %s, %s, %s, %s, %s)""", (small_results))
results_rows += c.rowcount
while timestamps:
small_timestamps, timestamps = timestamps[:100], timestamps[100:]
c.executemany("""INSERT INTO timestamps
(host,progress,timestamp)
VALUES (%s, %s, %s)""", (small_timestamps))
timestamps_rows += c.rowcount
db.commit()
print """Number of rows inserted: %d results""" % results_rows
print """Number of rows inserted: %d timestamps""" % timestamps_rows
def select_nbe(database, risk, order, sort):
"""Perform SQL SELECT query.
This section of code is used to perform a SQL SELECT query of Nessus data
already in a database specified using the -d option on the command line.
"""
import MySQLdb
print """Executing SQL SELECT..."""
try:
db = MySQLdb.connect(DB_HOST,DB_UNAME,DB_PASSWD,database)
except MySQLdb.Error, e:
print """Error %d: %s""" % (e.args[0], e.args[1])
sys.exit (1)
c = db.cursor()
c.execute("""SELECT domain, host, service, scriptid, riskval, msg1, msg2
FROM results WHERE riskval >= %s
ORDER BY %s %s""", (risk, order, sort))
results = c.fetchall()
return results
def count_nbe(database, risk):
"""Perform SQL SELECT query displaying plugins by count.
This function is similar to select_nbe(), except that it does not record
domain or host information, instead performs a tally of plugins by count.
It is only called when --count is specified with a database on the command
line.
"""
import MySQLdb
print """Executing SQL SELECT with COUNT..."""
try:
db = MySQLdb.connect(DB_HOST,DB_UNAME,DB_PASSWD,database)
except MySQLdb.Error, e:
print """Error %d: %s""" % (e.args[0], e.args[1])
sys.exit (1)
c = db.cursor()
c.execute("""SELECT riskval, COUNT(scriptid) AS count,
scriptid, msg1, msg2, service
FROM results GROUP BY scriptid HAVING riskval >= %s
ORDER BY riskval DESC, count DESC, scriptid DESC""", (risk))
results = c.fetchall()
return results
def write_csv(file,data):
"""Write to CSV file.
Used with the -o option on the command line.
"""
import csv
if data:
print """Writing""", file + """..."""
writer = csv.writer(open(file,"wb"))
writer.writerows(data)
else:
print """Error occurred while processing: no data to write!"""
def main():
"""The main() function that contains our use cases."""
if opt.infile and opt.database and opt.outfile:
results, timestamps = parse_nbe(opt.infile)
insert_nbe(results,timestamps,opt.database)
write_csv(opt.outfile,results)
elif opt.infile and opt.database:
results, timestamps = parse_nbe(opt.infile)
insert_nbe(results,timestamps,opt.database)
elif opt.database and opt.outfile:
if opt.count:
results = count_nbe(opt.database,opt.risk)
else:
results = select_nbe(opt.database,opt.risk,opt.order,opt.sort)
write_csv(opt.outfile,results)
elif opt.infile and opt.outfile:
results, timestamps = parse_nbe(opt.infile)
write_csv(opt.outfile,results)
else:
print parser.error("You are missing arguments, see usage or help")
if __name__ == "__main__":
from optparse import OptionParser, make_option
option_list = [
make_option("-d", "--database", dest="database",
help="query results from specified MySQL database"),
make_option("-f", "--file", dest="infile",
help="input nbe file to parse"),
make_option("-o", "--output-file", dest="outfile",
help="output to CSV file"),
make_option("-r", "--risk", type="choice", dest="risk", default="1",
help="minimum risk criticality to query",
choices=["1","2","3"]),
make_option("--count", action="store_true", dest="count",
help="output results by count"),
make_option("--order", type="choice", dest="order", default="host",
help="order database query by column",
choices=["host","service","scriptid","riskval"]),
make_option("--sort", type="choice", dest="sort", default="",
help="sort results descending", choices=["","desc"])
]
usage = """usage: tissynbe.py [options] args
tissynbe.py -d database -f results.nbe
tissynbe.py -d database -o output.csv
tissynbe.py -d database -o output.csv --order scriptid --sort desc
tissynbe.py -d database -o output.csv --count
tissynbe.py -f results.nbe -o output.csv
tissynbe.py -f results.nbe -d database -o output.csv"""
parser = OptionParser(usage,option_list=option_list)
opt, args = parser.parse_args()
main()

Some changes that have been suggested from the wonderful #python IRC channel, is to create a mapping of strings to be replaced and iterate through it.
Code would look like the following:
s = 'abcabc'foo = [ ('a','foo'), ('b','bar'), ('c','baz') ]
for i, j in foo: s = s.replace( i, j )
Since some of the string replacements depend on previous regex matches and other replacements, I will need to do some testing and perhaps modify some of the regular expression objects to account for this.
Ok, so I applied the changes. For anyone who’s interested, processing a 17MB nbe file takes about 14 seconds to output as a CSV on my Thinkpad T42, 1.7GHz Centrino. YMMV.
Added the following on line 130:
line[4] = strftime("%Y-%m-%d %H:%M:%S", strptime(line[4]))Keep getting this when attempting to execute py script tissynbe.py. I have tested “Hello World” py script to verify python is available.
Ran this…
./tissynbe.py -d nessus -f ./June18scan.nbe
Recevied this error…
File “./tissynbe.py”, line 109
with open(nbe, ‘rU’) as file:
^
SyntaxError: invalid syntax
What version of Python are you using? I am running Python 2.5.1, which the Python 2.5 branch includes the __future__ module with_statement.
Added a small check at line 248 to only output to a CSV file if there is data to be written.
Also added the following plugins to the FIX line:
* 11040
* 11822
* 11865
* 14674
Thanks for the code Marcin, I’ll get you some updated changes to review for version 2.0