Initial commit

This commit is contained in:
Dejan Peretin
2012-06-28 12:57:34 +02:00
commit ba372b729b
4 changed files with 993 additions and 0 deletions

210
LICENSE.txt Executable file
View File

@@ -0,0 +1,210 @@
Certain components of this software are provided or made available only
subject to the licenses under which such components were licensed to
HomeAway. The relevant components and corresponding licenses are listed
[in the folder in the distribution titled 'licenses']. In any event, the
disclaimer of warranty and limitation of liability provision in this
Agreement will apply to all Software in this distribution.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

324
NessusXMLRPC.py Executable file
View File

@@ -0,0 +1,324 @@
#!/usr/bin/python
"""
Copyright (c) 2010 HomeAway, Inc.
All rights reserved. http://www.homeaway.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
import xml.etree.ElementTree
from httplib import HTTPSConnection,CannotSendRequest,ImproperConnectionState
from urllib import urlencode
from random import randint
from time import sleep
from exceptions import Exception
# Arbitary minimum and maximum values for random sequence num
SEQMIN = 10000
SEQMAX = 99999
# Simple exceptions for error handling
class NessusError(Exception):
"""
Base exception.
"""
def __init__( self, info, contents ):
self.info = info
self.contents = contents
class RequestError(NessusError):
"""
General requests.
"""
pass
class LoginError(NessusError):
"""
Login.
"""
pass
class PolicyError(NessusError):
"""
Policies.
"""
pass
class ScanError(NessusError):
"""
Scans.
"""
pass
class ReportError(NessusError):
"""
Reports.
"""
pass
class ParseError(NessusError):
"""
Parsing XML.
"""
pass
class Scanner:
def __init__( self, host, port, login=None, password=None):
"""
Initialize the scanner instance by setting up a connection and authenticating
if credentials are provided.
@type host: string
@param host: The hostname of the running Nessus server.
@type port: number
@param port: The port number for the XMLRPC interface on the Nessus server.
@type login: string
@param login: The username for logging in to Nessus.
@type password: string
@param password: The password for logging in to Nessus.
"""
self.host = host
self.port = port
self.connection = self._connect( host, port )
self.headers = {"Content-type":"application/x-www-form-urlencoded","Accept":"text/plain"}
if login != None and password != None:
self.login( login, password )
def _connect( self, host, port ):
"""
Internal method for connecting to the target Nessus server.
@type host: string
@param host: The hostname of the running Nessus server.
@type port: number
@param port: The port number for the XMLRPC interface on the Nessus server.
"""
self.connection = HTTPSConnection( host, port )
def _request( self, method, target, params ):
"""
Internal method for submitting requests to the target Nessus server, rebuilding
the connection if needed.
@type method: string
@param method: The HTTP verb/method used in the request (almost always POST).
@type target: string
@param target: The target path (or function) of the request.
@type params: string
@param params: The URL encoded parameters used in the request.
"""
try:
if self.connection is None:
self._connect( self.host, self.port )
self.connection.request( method, target, params, self.headers )
except CannotSendRequest,ImproperConnectionState:
self._connect( self.host, self.port)
self.login( self.login, self.password )
self._request( method, target, params, self.headers )
return self.connection.getresponse().read()
def _rparse( self, parsed ):
"""
Recursively parse XML and generate an interable hybrid dictionary/list with all data.
@type parsed: xml.etree.ElementTree.Element
@param parsed: An ElementTree Element object of the parsed XML.
"""
result = dict()
# Iterate over each element
for element in parsed.getchildren():
# If the element has children, use a dictionary
children = element.getchildren()
if len(children) > 0:
# We have children for this element
if type(result) is list:
# Append the next parse, we're apparently in a list()
result.append(self._rparse( element ))
elif type(result) is dict and result.has_key(element.tag):
# Change the dict() to a list() if we have multiple hits
tmp = result
result = list()
# Iterate through the values in the dictionary, adding values only
# - This reduces redundancy in parsed output (no outer tags)
for val in tmp.itervalues():
result.append(val)
else:
result[element.tag] = dict()
result[element.tag] = self._rparse( element )
else:
result[element.tag] = element.text
return result
def parse( self, response ):
"""
Parse the XML response from the server.
@type response: string
@param response: Response XML from the server following a request.
"""
# Okay, for some reason there's a bug with how expat handles newlines
try:
return self._rparse( xml.etree.ElementTree.fromstring(response.replace("\n","")) )
except Exception:
raise ParseError( "Error parsing XML", response )
def login( self, login, password, seq=randint(SEQMIN,SEQMAX) ):
"""
Log in to the Nessus server and preserve the token value for subsequent requests.
@type login: string
@param login: The username for logging in to Nessus.
@type password: string
@param password: The password for logging in to Nessus.
@type seq: number
@param seq: A sequence number that will be echoed back for unique identification (optional).
"""
self.username = login
self.password = password
params = urlencode({ 'login':self.username, 'password':self.password, 'seq':seq})
response = self._request( "POST", "/login", params )
parsed = self.parse( response )
if parsed['status'] == "OK":
contents = parsed['contents']
self.token = contents['token'] # Actual token value
user = contents['user'] # User dict (admin status, user name)
self.isadmin = user['admin'] # Is the logged in user an admin?
self.headers["Cookie"] = "token=%s" % self.token # Persist token value for subsequent requests
else:
raise LoginError( "Unable to login", contents )
def logout( self, seq=randint(SEQMIN,SEQMAX) ):
"""
Log out of the Nessus server, invalidating the current token value. Returns True if successful, False if not.
@type seq: number
@param seq: A sequence number that will be echoed back for unique identification (optional).
"""
params = urlencode( {'seq':seq} )
response = self._request( "POST", "/logout", params)
parsed = self.parse( response )
if parsed['status'] == "OK" and parsed['contents'] == "OK":
return True
else:
return False
def policyList( self, seq=randint(SEQMIN,SEQMAX) ):
"""
List the current policies configured on the server and return a dict with the info.
@type seq: number
@param seq: A sequence number that will be echoed back for unique identification (optional).
"""
params = urlencode( {'seq':seq} )
response = self._request( "POST", "/policy/list", params)
parsed = self.parse( response )
if parsed['status'] == "OK":
contents = parsed['contents']
policies = contents['policies'] # Should be an iterable list of policies
else:
raise PolicyError( "Unable to get policy list", contents )
return policies
def scanNew( self, scan_name, target, policy_id, seq=randint(SEQMIN,SEQMAX)):
"""
Start up a new scan on the Nessus server immediately.
@type scan_name: string
@param scan_name: The desired name of the scan.
@type target: string
@param target: A Nessus-compatible target string (comma separation, CIDR notation, etc.)
@type policy_id: number
@param policy_id: The unique ID of the policy to be used in the scan.
@type seq: number
@param seq: A sequence number that will be echoed back for unique identification (optional).
"""
params = urlencode( {'target':target,'policy_id':policy_id,'scan_name':scan_name,'seq':seq} )
response = self._request( "POST", "/scan/new", params)
parsed = self.parse( response )
if parsed['status'] == "OK":
contents = parsed['contents']
return contents['scan'] # Return what you can about the scan
else:
raise ScanError("Unable to start scan", contents )
def quickScan( self, scan_name, target, policy_name, seq=randint(SEQMIN,SEQMAX)):
"""
Configure a new scan using a canonical name for the policy. Perform a lookup for the policy ID and configure the scan,
starting it immediately.
@type scan_name: string
@param scan_name: The desired name of the scan.
@type target: string
@param target: A Nessus-compatible target string (comma separation, CIDR notation, etc.)
@type policy_name: string
@param policy_name: The name of the policy to be used in the scan.
@type seq: number
@param seq: A sequence number that will be echoed back for unique identification (optional).
"""
policies = self.policyList()
if type(policies['policy']) is dict:
# There appears to be only one configured policy
policy = policies['policy']
if policy['policyName'] == policy_name:
policy_id = policy['policyID']
else:
raise PolicyError( "Unable to parse policies from policyList()", (scan_name,target,policy_name))
else:
# We have multiple policies configured
for policy in policies:
if policy['policyName'] == policy_name:
policy_id = policy['policyID']
return self.scanNew( scan_name, target, policy_id )
def reportList( self, seq=randint(SEQMIN,SEQMAX)):
"""
Generate a list of reports available on the Nessus server.
@type seq: number
@param seq: A sequence number that will be echoed back for unique identification (optional).
"""
params = urlencode({'seq':seq})
response = self._request( "POST", "/report/list", params)
parsed = self.parse( response )
if parsed['status'] == "OK":
contents = parsed['contents']
return contents['reports'] # Return an iterable list of reports
else:
raise ReportError( "Unable to get reports.", contents )
def reportDownload( self, report, version="v2" ):
"""
Download a report (XML) for a completed scan.
@type report: string
@param report: The UUID of the report or completed scan.
@type version: string
@param version: The version of the .nessus XML file you wish to download.
"""
if version == "v1":
params = urlencode({'report':report, 'v1':version })
else:
params = urlencode({'report':report})
return self._request( "POST", "/file/report/download", params )

24
nessus.conf Normal file
View File

@@ -0,0 +1,24 @@
# Defaults
[core]
server = nessus01
port = 8834
user = nessus
password = *pass*
logfile = /home/user/tools/nessus-xmlrpc/nessus.log
loglevel = debug
limit = 3
sleepmax = 600
sleepmin = 300
[smtp]
to = me@mydomain.com
from = security@mydomain.com
server = mysmtpserver
port = 25
[report]
outputdir = /home/user/tools/nessus-xmlrpc/reports
xsltproc = /usr/bin/xsltproc
xsltlog = /home/user/tools/nessus-xmlrpc/reports/xsltproc.log
xsl = /home/user/tools/nessus-xmlrpc/reports/html.xsl

435
nessus.py Executable file
View File

@@ -0,0 +1,435 @@
#!/usr/bin/env python
"""
Copyright (c) 2010 HomeAway, Inc.
All rights reserved. http://www.homeaway.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys,subprocess,shlex,os,smtplib,logging,socket,zipfile
import xml.etree.ElementTree
import ConfigParser
from NessusXMLRPC import Scanner,ParseError
from optparse import OptionParser
from random import randint
from time import sleep
from logging.handlers import WatchedFileHandler
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email import Encoders
from exceptions import KeyError
class Nessus:
def __init__( self, configfile, scans ):
"""
@type configfile: string
@param configfile: Full path to a configuration file for loading defaults
@type scans: list
@param scans: A list() of scans assembled with all necessary context
"""
self.logformat = "%s %8s %s"
self.scans_running = [] # Scans currently running.
self.scans_complete = [] # Scans that have completed.
self.scans = scans # Scans that remain to be started.
self.started = False # Flag for telling when scanning has started.
# Parse the configuration file to set everything up
self.config = ConfigParser.ConfigParser()
self.config.readfp(open(configfile))
loglevels = { 'debug' : logging.DEBUG,
'info' : logging.INFO,
'warning' : logging.WARNING,
'error' : logging.ERROR,
'critical': logging.CRITICAL }
# Core settings
self.logfile = self.config.get( 'core', 'logfile' )
self.loglevel = loglevels[self.config.get( 'core', 'loglevel' )]
# Setup some basic logging.
self.logger = logging.getLogger('Nessus')
self.logger.setLevel(self.loglevel)
self.loghandler = WatchedFileHandler( self.logfile )
self.logger.addHandler(self.loghandler)
self.debug( "CONF configfile = %s" % configfile )
self.debug( "Logger initiated; Logfile: %s, Loglevel: %s" % (self.logfile,self.loglevel))
self.server = self.config.get( 'core', 'server' )
self.debug( "CONF core.server = %s" % self.server)
self.port = self.config.getint( 'core', 'port' )
self.debug( "CONF core.port = %s" % self.port)
self.user = self.config.get( 'core', 'user' )
self.debug( "CONF core.user = %s" % self.user )
self.password = self.config.get( 'core', 'password' )
self.debug( "CONF core.password set" )
self.limit = self.config.getint( 'core', 'limit' )
self.debug( "CONF core.limit = %d" % self.limit )
self.sleepmax = self.config.getint( 'core', 'sleepmax')
self.debug( "CONF core.sleepmax = %d" % self.sleepmax )
self.sleepmin = self.config.getint( 'core', 'sleepmin')
self.debug( "CONF core.sleepmin = %d" % self.sleepmin )
# SMTP settings
self.emailto = self.config.get( 'smtp', 'to' )
self.debug( "CONF smtp.emailto = %s" % self.emailto )
self.emailfrom = self.config.get( 'smtp', 'from' )
self.debug( "CONF smtp.emailfrom = %s" % self.emailfrom )
self.smtpserver = self.config.get( 'smtp', 'server' )
self.debug( "CONF smtp.smtpserver = %s" % self.smtpserver )
self.smtpport = self.config.getint( 'smtp', 'port' )
self.debug( "CONF smtp.smtpport = %d" % self.smtpport )
# Reporting settings
self.reports = self.config.get( 'report', 'outputdir' )
self.debug( "CONF report.reports = %s" % self.reports )
self.xsltproc = self.config.get( 'report', 'xsltproc' )
self.debug( "CONF report.xsltproc = %s" % self.xsltproc )
self.xsltlog = self.config.get( 'report', 'xsltlog' )
self.debug( "CONF report.xsltlog = %s" % self.xsltlog )
self.xsl = self.config.get( 'report', 'xsl' )
self.debug( "CONF report.xsl = %s" % self.xsl )
self.debug( "PARSED scans: %s" % self.scans )
try:
self.info("Nessus scanner started.")
self.scanner = Scanner( self.server, self.port, self.user, self.password )
self.info("Connected to Nessus server; authenticated to server '%s' as user '%s'" % (self.server,self.user))
except socket.error as (errno,strerror):
self.error("Socket error encountered while connecting to Nessus server: %s. User: '%s', Server: '%s', Port: %s" % (strerror,self.user,self.server,self.port))
return None
def start( self ):
"""
Proxy for resume() really. Basically begins scanning with the current scanning list.
"""
self.started = True
if len(self.scans) > 1:
self.info("Starting with multiple scans")
else:
self.info("Starting with a single scan")
if self.scans_running is None:
self.scans_running = []
return self.resume()
def stop( self ):
"""
We have a start() so we most certainly should have a stop(). This should prevent scans from being continued.
"""
self.started = False
def resume( self ):
"""
Basically gets scans going, observing the limit.
"""
if self.started and len(self.scans) > 0 and len(self.scans_running) < self.limit:
count = len(self.scans_running)
for scan in self.scans:
if self._startscan(scan):
count += 1
if count == self.limit:
self.warning("Concurrent scan limit reached (currently set at %d)" % self.limit)
self.warning("Will monitor scans and continue as possible")
break
return self.scans_running
def _startscan( self, scan ):
"""
Start a specific scan in the scans list.
"""
currentscan = self.scanner.quickScan( scan['name'], scan['target'], scan['policy'] )
if currentscan is not None:
self.info("Scan successfully started; Owner: '%s', Name: '%s'" % (currentscan['owner'],currentscan['scan_name']))
else:
self.error("Unable to start scan. Name: '%s', Target: '%s', Policy: '%s'" % (scan['name'],scan['target'],scan['policy']))
return False
# Add the newly started scan to the running least, remove it from the remaining
self.scans_running.append(currentscan)
self.scans.remove(scan)
return True
def iscomplete( self ):
"""
Check for the completion of of running scans. Also, if there are scans left to be run, resume and run them.
"""
try:
reports = self.scanner.reportList()
except socket.error as (errno,strerror):
self.error("Socket error; %s" % strerror)
self.error("Invalidating connection and sleeping before we continue")
self.scanner.connection.close()
self.scanner.connection = None
sleep(self.sleepmax)
except ParseError as e:
self.error("%s; %s" % (e.info,e.contents))
self.error("Continuing...")
return False
for scan in self.scans_running:
try:
if type(reports) is dict:
# We have only one report
report = reports['report']
if report['status'] == 'completed' and scan['uuid'] == report['name']:
self.scans_complete.append(scan)
self.scans_running.remove(scan)
elif type(reports) is list:
# We have multiple reports to look through
for report in reports:
if report['status'] == 'completed' and scan['uuid'] == report['name']:
self.scans_complete.append(scan)
self.scans_running.remove(scan)
except KeyError:
self.error("KeyError when parsing XML from reportList(); continuing")
return False
# Check to see if we're running under the limit and we have scans remaining.
# If so, run more scans up to the limit and continue.
if len(self.scans_running)<self.limit and len(self.scans)>0 and self.started:
self.info("We can run more scans, resuming")
self.resume()
elif len(self.scans_running)>0:
return False
else:
return True
def report( self ):
"""
Report on currently completed scans.
"""
for scan in self.scans_complete:
pname = scan['scan_name'].replace(' ','')
data = self.scanner.reportDownload( scan['uuid'] )
xmlf = os.path.join( self.reports, pname+'.xml' )
htmlf = os.path.join( self.reports, pname+'.html')
zipf = os.path.join( self.reports, pname+'.zip' )
self.genreport( data, xmlf, htmlf, zipf )
self.info("XML report saved as '%s'" % xmlf)
self.info("HTML report saved as '%s'" % htmlf)
# Put together the text of the email with the report attached
self.send_report( "Report: %s" % scan['scan_name'], self.gensummary(data), zipf)
self.info("Email report sent to '%s' from '%s' including '%s'" % ( self.emailto,self.emailfrom,zipf))
def genreport( self, data, xmlf, htmlf, zipf ):
"""
Simple method for transforming the XML spit out by the server into report-style HTML using
what's available.
@type data: string
@param data: XML output from the report of a scan.
@type xmlf: string
@param xmlf: The file where the XML is to be output.
@type htmlf: string
@param htmlf: The file where the HTML is to be output.
@type zipf: string
@param zipf: The output ZipFile containing the compressed report.
"""
output = open( xmlf, "w")
output.write(data)
output.close()
xsltlog = open( self.xsltlog, 'w' )
# Transform the XML using the XSL provided by Nessus for HTML reports (quietly)
subprocess.call(shlex.split("%s %s %s -o %s" % (self.xsltproc,self.xsl,xmlf,htmlf)), stdout=xsltlog, stderr=xsltlog)
zip = zipfile.ZipFile( zipf, 'w' )
zip.write(htmlf,arcname=os.path.basename(htmlf))
zip.close()
xsltlog.close()
def gensummary( self, data ):
"""
Generate a simple summary as the contents of the email report to be sent.
@type data: string
@param data: XML data from the current report.
"""
severity = { '0' : 0,
'1' : 0,
'2' : 0,
'3' : 0 }
prefs = {}
pref = None
count = 0
parsed = xml.etree.ElementTree.fromstring(data)
# Pull out the report name
report = parsed.find("Report").attrib['name']
# Pull out the name of the policy used
policy = parsed.find("Policy/policyName").text
# Parse preferences and construct a dict from all settings
for preference in parsed.find("Policy/Preferences").getiterator("preference"):
for child in preference.getchildren():
if child.tag == 'name':
prefs[child.text] = None
pref = child.text
elif child.tag == 'value':
prefs[pref] = child.text
# Parse severity for totals
for host in parsed.find("Report").getiterator("ReportHost"):
for item in host.getiterator("ReportItem"):
severity[item.attrib['severity']] += 1
return "Scan Name: %25s\nTarget(s): %25s\nPolicy: %28s\n\nRisk Summary\n%s\n%15s %3s\n%15s %3s\n%15s %3s\n\n%15s %3s" % ( report, prefs['TARGET'], policy,'-'*36,'High', severity['3'], 'Medium', severity['2'], 'Low', severity['1'], 'Open Ports', severity['0'])
def send_report( self, subject, body, attachment, apptype='x/zip'):
"""
Send the email report to its destination.
@type to: string
@param to: Destination email address for the report.
@type subject: string
@param subject: The subject of the email message.
@type body: string
@param body: The body of the email message (includes report summary).
@type attachment: string
@param attachment: Path to report file for attaching to message.
@type apptype: string
@param apptype: Application MIME type for attachment.
"""
message = MIMEMultipart()
message['From'] = self.emailfrom
message['To'] = self.emailto
message['Subject'] = subject
message.attach( MIMEText( body ))
part = MIMEBase('application',apptype)
part.set_payload( open( attachment, 'r').read())
Encoders.encode_base64(part)
part.add_header('Content-Disposition','attachment; filename="%s"' % os.path.basename(attachment))
message.attach(part)
conn = smtplib.SMTP(self.smtpserver, self.smtpport)
conn.sendmail( message['From'], self.emailto, message.as_string())
conn.close()
def close( self ):
"""
End it.
"""
return self.scanner.logout()
def debug( self, msg ):
"""
@type msg: string
@param msg: Debug message to be written to the log.
"""
self.logger.debug( self.logformat % (datetime.now(),'DEBUG',msg))
def info( self, msg ):
"""
@type msg: string
@param msg: Info message to be written to the log.
"""
self.logger.info( self.logformat % (datetime.now(),'INFO',msg))
def warning( self, msg ):
"""
@type msg: string
@param msg: Warning message to be written to the log.
"""
self.logger.warning( self.logformat % (datetime.now(),'WARNING',msg))
def error( self, msg ):
"""
@type msg: string
@param msg: Error message to be written to the log.
"""
self.logger.info( self.logformat % (datetime.now(),'ERROR',msg))
def critical( self, msg ):
"""
@type msg: string
@param msg: Critical message to be written to the log.
"""
self.logger.critical( self.logformat % (datetime.now(),'CRITICAL',msg))
#############################################################################################################
if __name__ == "__main__":
"""
The goal with this tool is to essentially replace the command-line versions of the Nessus scanner. I
found with the latest version that they've deprecated version 1 of the Nessus XML output preventing
policies exported directly through the web interface from being used with the command-line versions to
automate scans. This tool is an example using the NessusXMLRPC module I've also written to completely
automate scans using the Nessus server. For more info, review the help/usage information available.
nessus.py and NessusXMLRPC were written under Python v2.6.5 with xsltproc available in the PATH. Feel
free to tweak the default concurrent scanning limit in configuration file; what's currently set is
what worked the best on my test box. The Nessus daemon appears to be touchy when it comes to resources.
"""
parser = OptionParser()
parser.add_option("-t", dest='target', help="target string for Nessus scan")
parser.add_option("-n", dest='name', default="No-name Auto Scan", help="name for the scan")
parser.add_option("-p", dest='policy', help="policy (on server-side) to use in the scan")
parser.add_option("-f", dest='infile', help="input file with multiple scans to run")
parser.add_option("-c", dest='configfile', default='nessus.conf', help="configuration file to use")
(options,args) = parser.parse_args()
if options.configfile is not None and \
(options.infile is not None or options.target is not None):
if options.infile is not None and options.target is None:
# Start with multiple scans.
scans = []
f = open(options.infile, "r")
for line in f:
scan = line.strip().split(',')
scans.append({'name':scan[0],'target':scan[1],'policy':scan[2]})
x = Nessus( options.configfile, scans )
scans = x.start()
elif options.target is not None and options.infile is None:
# Start with a single scan.
if options.name is not None and \
options.target is not None and \
options.policy is not None:
scan = [{ 'name' : options.name, 'target' : options.target, 'policy' : options.policy }]
x = Nessus( options.configfile, scan )
scans = x.start()
else:
print "HARD ERROR: Incorrect usage.\n"
parser.print_help()
sys.exit(1)
while True:
if scans is None:
break
sleeptime = randint(x.sleepmin,x.sleepmax)
x.info("Sleeping for %d seconds, polling for scan completion" % sleeptime)
sleep(sleeptime)
if x.iscomplete():
x.report()
break
x.info("All done; closing")
x.close()
sys.exit(0)
else:
parser.print_help()
sys.exit(0)