Refactored classes to be more modular, update to ini file and submodules
This commit is contained in:
@ -21,20 +21,24 @@ def main():
|
|||||||
your vulnerability scans through aggregation of historical scans.""")
|
your vulnerability scans through aggregation of historical scans.""")
|
||||||
parser.add_argument('-c', '--config', dest='config', required=False, default='frameworks.ini',
|
parser.add_argument('-c', '--config', dest='config', required=False, default='frameworks.ini',
|
||||||
help='Path of config file', type=lambda x: isFileValid(parser, x.strip()))
|
help='Path of config file', type=lambda x: isFileValid(parser, x.strip()))
|
||||||
|
parser.add_argument('-s', '--section', dest='section', required=False,
|
||||||
|
help='Section in config')
|
||||||
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', default=True,
|
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', default=True,
|
||||||
help='Prints status out to screen (defaults to True)')
|
help='Prints status out to screen (defaults to True)')
|
||||||
parser.add_argument('-u', '--username', dest='username', required=False, default=None, type=lambda x: x.strip(), help='The NESSUS username')
|
parser.add_argument('-u', '--username', dest='username', required=False, default=None, type=lambda x: x.strip(), help='The NESSUS username')
|
||||||
parser.add_argument('-p', '--password', dest='password', required=False, default=None, type=lambda x: x.strip(), help='The NESSUS password')
|
parser.add_argument('-p', '--password', dest='password', required=False, default=None, type=lambda x: x.strip(), help='The NESSUS password')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
vw = vulnWhisperer(config=args.config,
|
vw = vulnWhisperer(config=args.config,
|
||||||
|
profile=args.section,
|
||||||
verbose=args.verbose,
|
verbose=args.verbose,
|
||||||
username=args.username,
|
username=args.username,
|
||||||
password=args.password)
|
password=args.password)
|
||||||
|
|
||||||
vw.whisper_nessus()
|
vw.whisper_vulnerabilities()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -43,6 +47,5 @@ def main():
|
|||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
@ -11,16 +11,18 @@ verbose=true
|
|||||||
|
|
||||||
[qualys]
|
[qualys]
|
||||||
#Reference https://www.qualys.com/docs/qualys-was-api-user-guide.pdf to find your API
|
#Reference https://www.qualys.com/docs/qualys-was-api-user-guide.pdf to find your API
|
||||||
|
enabled = true
|
||||||
hostname = qualysapi.qg2.apps.qualys.com
|
hostname = qualysapi.qg2.apps.qualys.com
|
||||||
username = exampleuser
|
username = exampleuser
|
||||||
password = examplepass
|
password = examplepass
|
||||||
write_path=/opt/vulnwhisp/qualys/
|
write_path=/opt/vulnwhisp/qualys/
|
||||||
db_path=/opt/vulnwhisp/database/
|
db_path=/opt/vulnwhisp/database
|
||||||
verbose=true
|
verbose=true
|
||||||
|
|
||||||
# Set the maximum number of retries each connection should attempt. Note, this applies only to failed connections and timeouts, never to requests where the server returns a response.
|
# Set the maximum number of retries each connection should attempt.
|
||||||
|
#Note, this applies only to failed connections and timeouts, never to requests where the server returns a response.
|
||||||
max_retries = 10
|
max_retries = 10
|
||||||
template_id = = 126024
|
template_id = 126024
|
||||||
|
|
||||||
#[proxy]
|
#[proxy]
|
||||||
; This section is optional. Leave it out if you're not using a proxy.
|
; This section is optional. Leave it out if you're not using a proxy.
|
||||||
|
7
deps/qualysapi/qualysapi/config.py
vendored
7
deps/qualysapi/qualysapi/config.py
vendored
@ -94,8 +94,8 @@ class QualysConnectConfig:
|
|||||||
logger.error('Report Template ID Must be set and be an integer')
|
logger.error('Report Template ID Must be set and be an integer')
|
||||||
print('Value template ID must be an integer.')
|
print('Value template ID must be an integer.')
|
||||||
exit(1)
|
exit(1)
|
||||||
self._cfgparse.set('qualys', 'template_id', str(self.max_retries))
|
self._cfgparse.set('qualys', 'template_id', str(self.report_template_id))
|
||||||
self.max_retries = int(self.max_retries)
|
self.report_template_id = int(self.report_template_id)
|
||||||
|
|
||||||
# Proxy support
|
# Proxy support
|
||||||
proxy_config = proxy_url = proxy_protocol = proxy_port = proxy_username = proxy_password = None
|
proxy_config = proxy_url = proxy_protocol = proxy_port = proxy_username = proxy_password = None
|
||||||
@ -216,3 +216,6 @@ class QualysConnectConfig:
|
|||||||
def get_hostname(self):
|
def get_hostname(self):
|
||||||
''' Returns hostname. '''
|
''' Returns hostname. '''
|
||||||
return self._cfgparse.get('qualys', 'hostname')
|
return self._cfgparse.get('qualys', 'hostname')
|
||||||
|
|
||||||
|
def get_template_id(self):
|
||||||
|
return self._cfgparse.get('qualys','template_id')
|
||||||
|
12
deps/qualysapi/qualysapi/connector.py
vendored
12
deps/qualysapi/qualysapi/connector.py
vendored
@ -9,7 +9,12 @@ and requesting data from it.
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
except ImportError:
|
||||||
|
from urlparse import urlparse
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -154,7 +159,7 @@ class QGConnector(api_actions.QGActions):
|
|||||||
if api_call_endpoint in self.api_methods['was get']:
|
if api_call_endpoint in self.api_methods['was get']:
|
||||||
return 'get'
|
return 'get'
|
||||||
# Post calls with no payload will result in HTTPError: 415 Client Error: Unsupported Media Type.
|
# Post calls with no payload will result in HTTPError: 415 Client Error: Unsupported Media Type.
|
||||||
if not data:
|
if data is None:
|
||||||
# No post data. Some calls change to GET with no post data.
|
# No post data. Some calls change to GET with no post data.
|
||||||
if api_call_endpoint in self.api_methods['was no data get']:
|
if api_call_endpoint in self.api_methods['was no data get']:
|
||||||
return 'get'
|
return 'get'
|
||||||
@ -215,7 +220,8 @@ class QGConnector(api_actions.QGActions):
|
|||||||
data = data.lstrip('?')
|
data = data.lstrip('?')
|
||||||
data = data.rstrip('&')
|
data = data.rstrip('&')
|
||||||
# Convert to dictionary.
|
# Convert to dictionary.
|
||||||
data = urllib.parse.parse_qs(data)
|
#data = urllib.parse.parse_qs(data)
|
||||||
|
data = urlparse(data)
|
||||||
logger.debug('Converted:\n%s' % str(data))
|
logger.debug('Converted:\n%s' % str(data))
|
||||||
elif api_version in ('am', 'was', 'am2'):
|
elif api_version in ('am', 'was', 'am2'):
|
||||||
if type(data) == etree._Element:
|
if type(data) == etree._Element:
|
||||||
|
6
deps/qualysapi/setup.py
vendored
6
deps/qualysapi/setup.py
vendored
@ -1,9 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import os
|
import os
|
||||||
import setuptools
|
import setuptools
|
||||||
import sys
|
|
||||||
try:
|
try:
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -35,7 +34,8 @@ setup(name=__pkgname__,
|
|||||||
keywords='Qualys QualysGuard API helper network security',
|
keywords='Qualys QualysGuard API helper network security',
|
||||||
url='https://github.com/austin-taylor/qualysapi',
|
url='https://github.com/austin-taylor/qualysapi',
|
||||||
package_dir={'': '.'},
|
package_dir={'': '.'},
|
||||||
packages=setuptools.find_packages(),,
|
#packages=setuptools.find_packages(),
|
||||||
|
packages=['qualysapi',],
|
||||||
# package_data={'qualysapi':['LICENSE']},
|
# package_data={'qualysapi':['LICENSE']},
|
||||||
# scripts=['src/scripts/qhostinfo.py', 'src/scripts/qscanhist.py', 'src/scripts/qreports.py'],
|
# scripts=['src/scripts/qhostinfo.py', 'src/scripts/qscanhist.py', 'src/scripts/qreports.py'],
|
||||||
long_description=read('README.md'),
|
long_description=read('README.md'),
|
||||||
|
@ -1,141 +0,0 @@
|
|||||||
# Author: Austin Taylor and Justin Henderson
|
|
||||||
# Email: email@austintaylor.io
|
|
||||||
# Last Update: 05/22/2017
|
|
||||||
# Version 0.2
|
|
||||||
# Description: Take in nessus reports from vulnWhisperer and pumps into logstash
|
|
||||||
#Replace "filebeathost" with the name of your computer
|
|
||||||
|
|
||||||
input {
|
|
||||||
beats {
|
|
||||||
port => 5044
|
|
||||||
tags => "beats"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filter {
|
|
||||||
if [beat][hostname] == "filebeathost" {
|
|
||||||
mutate {
|
|
||||||
add_tag => ["nessus"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
filter {
|
|
||||||
if "nessus" in [tags]{
|
|
||||||
mutate {
|
|
||||||
gsub => [
|
|
||||||
"message", "\|\|\|", " ",
|
|
||||||
"message", "\t\t", " ",
|
|
||||||
"message", " ", " ",
|
|
||||||
"message", " ", " ",
|
|
||||||
"message", " ", " "
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
csv {
|
|
||||||
columns => ["plugin_id", "cve", "cvss", "risk", "host", "protocol", "port", "plugin_name", "synopsis", "description", "solution", "see_also", "plugin_output"]
|
|
||||||
separator => ","
|
|
||||||
source => "message"
|
|
||||||
}
|
|
||||||
|
|
||||||
grok {
|
|
||||||
match => { "source" => "(?<file_path>[\\\:a-z A-Z_]*\\)(?<scan_name>[a-z-0-9\.A-Z_\-]*)_%{INT:scan_id}_%{INT:history_id}_%{INT:last_updated}" }
|
|
||||||
tag_on_failure => []
|
|
||||||
}
|
|
||||||
date {
|
|
||||||
match => [ "last_updated" , "UNIX" ]
|
|
||||||
target => "@timestamp"
|
|
||||||
remove_field => ["last_updated"]
|
|
||||||
}
|
|
||||||
if [risk] == "None" {
|
|
||||||
mutate { add_field => { "risk_number" => 0 }}
|
|
||||||
}
|
|
||||||
if [risk] == "Low" {
|
|
||||||
mutate { add_field => { "risk_number" => 1 }}
|
|
||||||
}
|
|
||||||
if [risk] == "Medium" {
|
|
||||||
mutate { add_field => { "risk_number" => 2 }}
|
|
||||||
}
|
|
||||||
if [risk] == "High" {
|
|
||||||
mutate { add_field => { "risk_number" => 3 }}
|
|
||||||
}
|
|
||||||
if [risk] == "Critical" {
|
|
||||||
mutate { add_field => { "risk_number" => 4 }}
|
|
||||||
}
|
|
||||||
if [cve] == "nan" {
|
|
||||||
mutate { remove_field => [ "cve" ] }
|
|
||||||
}
|
|
||||||
if [see_also] == "nan" {
|
|
||||||
mutate { remove_field => [ "see_also" ] }
|
|
||||||
}
|
|
||||||
if [description] == "nan" {
|
|
||||||
mutate { remove_field => [ "description" ] }
|
|
||||||
}
|
|
||||||
if [plugin_output] == "nan" {
|
|
||||||
mutate { remove_field => [ "plugin_output" ] }
|
|
||||||
}
|
|
||||||
if [synopsis] == "nan" {
|
|
||||||
mutate { remove_field => [ "synopsis" ] }
|
|
||||||
}
|
|
||||||
|
|
||||||
mutate {
|
|
||||||
remove_field => [ "message" ]
|
|
||||||
add_field => { "risk_score" => "%{cvss}" }
|
|
||||||
}
|
|
||||||
mutate {
|
|
||||||
convert => { "risk_score" => "float" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Compensating controls - adjust risk_score
|
|
||||||
# Adobe and Java are not allowed to run in browser unless whitelisted
|
|
||||||
# Therefore, lower score by dividing by 3 (score is subjective to risk)
|
|
||||||
if [risk_score] != 0 {
|
|
||||||
if [plugin_name] =~ "Adobe" and [risk_score] > 6 or [plugin_name] =~ "Java" and [risk_score] > 6 {
|
|
||||||
ruby {
|
|
||||||
code => "event.set('risk_score', event.get('risk_score') / 3)"
|
|
||||||
}
|
|
||||||
mutate {
|
|
||||||
add_field => { "compensating_control" => "Adobe and Flash removed from browsers unless whitelisted site." }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add tags for reporting based on assets or criticality
|
|
||||||
if [host] == "192.168.0.1" or [host] == "192.168.0.50" or [host] =~ "^192\.168\.10\." or [host] =~ "^42.42.42." {
|
|
||||||
mutate {
|
|
||||||
add_tag => [ "critical_asset" ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if [host] =~ "^192\.168\.[45][0-9][0-9]\.1$" or [host] =~ "^192.168\.[50]\.[0-9]{1,2}\.1$"{
|
|
||||||
mutate {
|
|
||||||
add_tag => [ "has_hipaa_data" ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if [host] =~ "^192\.168\.[45][0-9][0-9]\." {
|
|
||||||
mutate {
|
|
||||||
add_tag => [ "hipaa_asset" ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if [host] =~ "^192\.168\.5\." {
|
|
||||||
mutate {
|
|
||||||
add_tag => [ "pci_asset" ]
|
|
||||||
}
|
|
||||||
if [host] =~ "^10\.0\.50\." {
|
|
||||||
mutate {
|
|
||||||
add_tag => [ "web_servers" ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output {
|
|
||||||
if "nessus" in [tags] or [type] == "nessus" {
|
|
||||||
#stdout { codec => rubydebug }
|
|
||||||
elasticsearch {
|
|
||||||
hosts => [ "localhost" ]
|
|
||||||
index => "logstash-nessus-%{+YYYY.MM}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,7 +7,7 @@ output {
|
|||||||
if "nessus" in [tags] or [type] == "nessus" {
|
if "nessus" in [tags] or [type] == "nessus" {
|
||||||
#stdout { codec => rubydebug }
|
#stdout { codec => rubydebug }
|
||||||
elasticsearch {
|
elasticsearch {
|
||||||
hosts => "localhost:19200"
|
hosts => "localhost:9200"
|
||||||
index => "logstash-nessus-%{+YYYY.MM}"
|
index => "logstash-nessus-%{+YYYY.MM}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
from utils.cli import *
|
from utils.cli import bcolors
|
@ -35,7 +35,7 @@ class NessusAPI(object):
|
|||||||
'Origin': self.base,
|
'Origin': self.base,
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
'Accept-Language': 'en-US,en;q=0.8',
|
'Accept-Language': 'en-US,en;q=0.8',
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36',
|
'User-Agent': 'VulnWhisperer for Nessus',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||||
'Referer': self.base,
|
'Referer': self.base,
|
||||||
|
@ -6,6 +6,7 @@ from lxml import objectify
|
|||||||
from lxml.builder import E
|
from lxml.builder import E
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import qualysapi
|
||||||
import qualysapi.config as qcconf
|
import qualysapi.config as qcconf
|
||||||
import requests
|
import requests
|
||||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||||||
@ -46,6 +47,7 @@ class qualysWhisper(object):
|
|||||||
self.template_id = self.config_parse.get_template_id()
|
self.template_id = self.config_parse.get_template_id()
|
||||||
except:
|
except:
|
||||||
print 'ERROR - Could not retrieve template ID'
|
print 'ERROR - Could not retrieve template ID'
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
def request(
|
def request(
|
||||||
self,
|
self,
|
||||||
@ -370,28 +372,15 @@ class qualysWebAppReport:
|
|||||||
if 'Content' not in merged_df:
|
if 'Content' not in merged_df:
|
||||||
merged_df['Content'] = ''
|
merged_df['Content'] = ''
|
||||||
|
|
||||||
merged_df['Payload #1'] = merged_df['Payload #1'
|
columns_to_cleanse = ['Payload #1','Request Method #1','Request URL #1',
|
||||||
].apply(self.cleanser)
|
'Request Headers #1','Response #1','Evidence #1',
|
||||||
merged_df['Request Method #1'] = merged_df['Request Method #1'
|
'Description','Impact','Solution','Url','Content']
|
||||||
].apply(self.cleanser)
|
|
||||||
merged_df['Request URL #1'] = merged_df['Request URL #1'
|
for col in columns_to_cleanse:
|
||||||
].apply(self.cleanser)
|
merged_df[col] = merged_df[col].apply(self.cleanser)
|
||||||
merged_df['Request Headers #1'] = merged_df['Request Headers #1'
|
|
||||||
].apply(self.cleanser)
|
|
||||||
merged_df['Response #1'] = merged_df['Response #1'
|
|
||||||
].apply(self.cleanser)
|
|
||||||
merged_df['Evidence #1'] = merged_df['Evidence #1'
|
|
||||||
].apply(self.cleanser)
|
|
||||||
|
|
||||||
merged_df['Description'] = merged_df['Description'
|
|
||||||
].apply(self.cleanser)
|
|
||||||
merged_df['Impact'] = merged_df['Impact'].apply(self.cleanser)
|
|
||||||
merged_df['Solution'] = merged_df['Solution'
|
|
||||||
].apply(self.cleanser)
|
|
||||||
merged_df['Url'] = merged_df['Url'].apply(self.cleanser)
|
|
||||||
merged_df['Content'] = merged_df['Content'].apply(self.cleanser)
|
|
||||||
merged_df = merged_df.drop(['QID_y', 'QID_x'], axis=1)
|
merged_df = merged_df.drop(['QID_y', 'QID_x'], axis=1)
|
||||||
|
|
||||||
merged_df = merged_df.rename(columns={'Id': 'QID'})
|
merged_df = merged_df.rename(columns={'Id': 'QID'})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -427,49 +416,15 @@ class qualysWebAppReport:
|
|||||||
|
|
||||||
return merged_data
|
return merged_data
|
||||||
|
|
||||||
def whisper_webapp(self, report_id, updated_date):
|
|
||||||
"""
|
|
||||||
report_id: App ID
|
|
||||||
updated_date: Last time scan was ran for app_id
|
|
||||||
"""
|
|
||||||
vuln_ready = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
|
|
||||||
if 'Z' in updated_date:
|
|
||||||
updated_date = self.iso_to_epoch(updated_date)
|
|
||||||
report_name = 'qualys_web_' + str(report_id) \
|
|
||||||
+ '_{last_updated}'.format(last_updated=updated_date) \
|
|
||||||
+ '.csv'
|
|
||||||
if os.path.isfile(report_name):
|
|
||||||
print('[ACTION] - File already exist! Skipping...')
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
print('[ACTION] - Generating report for %s' % report_id)
|
|
||||||
status = self.qw.create_report(report_id)
|
|
||||||
root = objectify.fromstring(status)
|
|
||||||
if root.responseCode == 'SUCCESS':
|
|
||||||
print('[INFO] - Successfully generated report for webapp: %s' \
|
|
||||||
% report_id)
|
|
||||||
generated_report_id = root.data.Report.id
|
|
||||||
print ('[INFO] - New Report ID: %s' \
|
|
||||||
% generated_report_id)
|
|
||||||
vuln_ready = self.process_data(generated_report_id)
|
|
||||||
|
|
||||||
vuln_ready.to_csv(report_name, index=False, header=True) # add when timestamp occured
|
|
||||||
print('[SUCCESS] - Report written to %s' \
|
|
||||||
% report_name)
|
|
||||||
print('[ACTION] - Removing report %s' \
|
|
||||||
% generated_report_id)
|
|
||||||
cleaning_up = \
|
|
||||||
self.qw.delete_report(generated_report_id)
|
|
||||||
os.remove(str(generated_report_id) + '.csv')
|
|
||||||
print('[ACTION] - Deleted report: %s' \
|
|
||||||
% generated_report_id)
|
|
||||||
else:
|
|
||||||
print('Could not process report ID: %s' % status)
|
|
||||||
except Exception as e:
|
|
||||||
print('[ERROR] - Could not process %s - %s' % (report_id, e))
|
|
||||||
return vuln_ready
|
|
||||||
|
|
||||||
|
|
||||||
|
maxInt = sys.maxsize
|
||||||
|
decrement = True
|
||||||
|
|
||||||
|
while decrement:
|
||||||
|
decrement = False
|
||||||
|
try:
|
||||||
|
csv.field_size_limit(maxInt)
|
||||||
|
except OverflowError:
|
||||||
|
maxInt = int(maxInt/10)
|
||||||
|
decrement = True
|
@ -12,5 +12,6 @@ class bcolors:
|
|||||||
UNDERLINE = '\033[4m'
|
UNDERLINE = '\033[4m'
|
||||||
|
|
||||||
INFO = '{info}[INFO]{endc}'.format(info=OKBLUE, endc=ENDC)
|
INFO = '{info}[INFO]{endc}'.format(info=OKBLUE, endc=ENDC)
|
||||||
|
ACTION = '{info}[ACTION]{endc}'.format(info=OKBLUE, endc=ENDC)
|
||||||
SUCCESS = '{green}[SUCCESS]{endc}'.format(green=OKGREEN, endc=ENDC)
|
SUCCESS = '{green}[SUCCESS]{endc}'.format(green=OKGREEN, endc=ENDC)
|
||||||
FAIL = '{red}[FAIL]{endc}'.format(red=FAIL, endc=ENDC)
|
FAIL = '{red}[FAIL]{endc}'.format(red=FAIL, endc=ENDC)
|
||||||
|
@ -4,8 +4,10 @@ __author__ = 'Austin Taylor'
|
|||||||
|
|
||||||
from base.config import vwConfig
|
from base.config import vwConfig
|
||||||
from frameworks.nessus import NessusAPI
|
from frameworks.nessus import NessusAPI
|
||||||
|
from frameworks.qualys import qualysWebAppReport
|
||||||
from utils.cli import bcolors
|
from utils.cli import bcolors
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from lxml import objectify
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import io
|
import io
|
||||||
@ -18,6 +20,9 @@ import logging
|
|||||||
|
|
||||||
|
|
||||||
class vulnWhispererBase(object):
|
class vulnWhispererBase(object):
|
||||||
|
|
||||||
|
CONFIG_SECTION = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config=None,
|
config=None,
|
||||||
@ -27,84 +32,31 @@ class vulnWhispererBase(object):
|
|||||||
debug=False,
|
debug=False,
|
||||||
username=None,
|
username=None,
|
||||||
password=None,
|
password=None,
|
||||||
):
|
section=None,
|
||||||
pass
|
):
|
||||||
|
|
||||||
class vulnWhisperer(object):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
config=None,
|
|
||||||
db_name='report_tracker.db',
|
|
||||||
purge=False,
|
|
||||||
verbose=None,
|
|
||||||
debug=False,
|
|
||||||
username=None,
|
|
||||||
password=None,
|
|
||||||
):
|
|
||||||
|
|
||||||
self.verbose = verbose
|
if self.CONFIG_SECTION is None:
|
||||||
self.nessus_connect = False
|
raise Exception('Implementing class must define CONFIG_SECTION')
|
||||||
self.develop = True
|
|
||||||
|
self.db_name = db_name
|
||||||
self.purge = purge
|
self.purge = purge
|
||||||
|
|
||||||
if config is not None:
|
if config is not None:
|
||||||
try:
|
self.config = vwConfig(config_in=config)
|
||||||
self.config = vwConfig(config_in=config)
|
self.enabled = self.config.get(self.CONFIG_SECTION, 'enabled')
|
||||||
self.nessus_enabled = self.config.getbool('nessus',
|
self.hostname = self.config.get(self.CONFIG_SECTION, 'hostname')
|
||||||
'enabled')
|
self.username = self.config.get(self.CONFIG_SECTION, 'username')
|
||||||
|
self.password = self.config.get(self.CONFIG_SECTION, 'password')
|
||||||
|
self.write_path = self.config.get(self.CONFIG_SECTION, 'write_path')
|
||||||
|
self.db_path = self.config.get(self.CONFIG_SECTION, 'db_path')
|
||||||
|
self.verbose = self.config.getbool(self.CONFIG_SECTION, 'verbose')
|
||||||
|
|
||||||
if self.nessus_enabled:
|
|
||||||
self.nessus_hostname = self.config.get('nessus',
|
|
||||||
'hostname')
|
|
||||||
self.nessus_port = self.config.get('nessus', 'port')
|
|
||||||
|
|
||||||
if password:
|
|
||||||
self.nessus_password = password
|
|
||||||
else:
|
|
||||||
self.nessus_password = self.config.get('nessus'
|
|
||||||
, 'password')
|
|
||||||
|
|
||||||
if username:
|
if self.db_name is not None:
|
||||||
self.nessus_username = username
|
if self.db_path:
|
||||||
else:
|
self.database = os.path.join(self.db_path,
|
||||||
self.nessus_username = self.config.get('nessus'
|
|
||||||
, 'username')
|
|
||||||
|
|
||||||
self.nessus_writepath = self.config.get('nessus',
|
|
||||||
'write_path')
|
|
||||||
self.nessus_dbpath = self.config.get('nessus',
|
|
||||||
'db_path')
|
|
||||||
self.nessus_trash = self.config.getbool('nessus',
|
|
||||||
'trash')
|
|
||||||
self.verbose = self.config.getbool('nessus',
|
|
||||||
'verbose')
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.vprint('{info} Attempting to connect to nessus...'.format(info=bcolors.INFO))
|
|
||||||
self.nessus = \
|
|
||||||
NessusAPI(hostname=self.nessus_hostname,
|
|
||||||
port=self.nessus_port,
|
|
||||||
username=self.nessus_username,
|
|
||||||
password=self.nessus_password)
|
|
||||||
self.nessus_connect = True
|
|
||||||
self.vprint('{success} Connected to nessus on {host}:{port}'.format(success=bcolors.SUCCESS,
|
|
||||||
host=self.nessus_hostname,
|
|
||||||
port=str(self.nessus_port)))
|
|
||||||
except Exception as e:
|
|
||||||
self.vprint(e)
|
|
||||||
raise Exception(
|
|
||||||
'{fail} Could not connect to nessus -- Please verify your settings in {config} are correct and try again.\nReason: {e}'.format(
|
|
||||||
config=self.config,
|
|
||||||
fail=bcolors.FAIL, e=e))
|
|
||||||
except Exception as e:
|
|
||||||
|
|
||||||
self.vprint('{fail} Could not properly load your config!\nReason: {e}'.format(fail=bcolors.FAIL,
|
|
||||||
e=e))
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
if db_name is not None:
|
|
||||||
if self.nessus_dbpath:
|
|
||||||
self.database = os.path.join(self.nessus_dbpath,
|
|
||||||
db_name)
|
db_name)
|
||||||
else:
|
else:
|
||||||
self.database = \
|
self.database = \
|
||||||
@ -137,6 +89,7 @@ class vulnWhisperer(object):
|
|||||||
'uuid',
|
'uuid',
|
||||||
'processed',
|
'processed',
|
||||||
]
|
]
|
||||||
|
|
||||||
self.init()
|
self.init()
|
||||||
self.uuids = self.retrieve_uuids()
|
self.uuids = self.retrieve_uuids()
|
||||||
self.processed = 0
|
self.processed = 0
|
||||||
@ -145,11 +98,14 @@ class vulnWhisperer(object):
|
|||||||
|
|
||||||
def vprint(self, msg):
|
def vprint(self, msg):
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print msg
|
print(msg)
|
||||||
|
|
||||||
def create_table(self):
|
def create_table(self):
|
||||||
self.cur.execute(
|
self.cur.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS scan_history (id INTEGER PRIMARY KEY, scan_name TEXT, scan_id INTEGER, last_modified DATE, filename TEXT, download_time DATE, record_count INTEGER, source TEXT, uuid TEXT, processed INTEGER)'
|
'CREATE TABLE IF NOT EXISTS scan_history (id INTEGER PRIMARY KEY,'
|
||||||
|
' scan_name TEXT, scan_id INTEGER, last_modified DATE, filename TEXT,'
|
||||||
|
' download_time DATE, record_count INTEGER, source TEXT,'
|
||||||
|
' uuid TEXT, processed INTEGER)'
|
||||||
)
|
)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
@ -168,10 +124,83 @@ class vulnWhisperer(object):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def path_check(self, _data):
|
def path_check(self, _data):
|
||||||
if self.nessus_writepath:
|
if self.write_path:
|
||||||
data = self.nessus_writepath + '/' + _data
|
data = self.write_path + '/' + _data
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def record_insert(self, record):
|
||||||
|
self.cur.execute('insert into scan_history({table_columns}) values (?,?,?,?,?,?,?,?,?)'.format(
|
||||||
|
table_columns=', '.join(self.table_columns)),
|
||||||
|
record)
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def retrieve_uuids(self):
|
||||||
|
"""
|
||||||
|
Retrieves UUIDs from database and checks list to determine which files need to be processed.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.conn.text_factory = str
|
||||||
|
self.cur.execute('SELECT uuid FROM scan_history where source = {config_section}'.format(config_section=self.CONFIG_SECTION))
|
||||||
|
results = frozenset([r[0] for r in self.cur.fetchall()])
|
||||||
|
except:
|
||||||
|
results = []
|
||||||
|
return results
|
||||||
|
|
||||||
|
class vulnWhispererNessus(vulnWhispererBase):
|
||||||
|
|
||||||
|
CONFIG_SECTION = 'nessus'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config=None,
|
||||||
|
db_name='report_tracker.db',
|
||||||
|
purge=False,
|
||||||
|
verbose=None,
|
||||||
|
debug=False,
|
||||||
|
username=None,
|
||||||
|
password=None,
|
||||||
|
):
|
||||||
|
super(vulnWhispererNessus, self).__init__(config=config)
|
||||||
|
|
||||||
|
self.port = int(self.config.get(self.CONFIG_NAME, 'port'))
|
||||||
|
|
||||||
|
self.develop = True
|
||||||
|
self.purge = purge
|
||||||
|
|
||||||
|
if config is not None:
|
||||||
|
try:
|
||||||
|
#if self.enabled:
|
||||||
|
self.nessus_port = self.config.get(self.CONFIG_SECTION, 'port')
|
||||||
|
|
||||||
|
self.nessus_trash = self.config.getbool(self.CONFIG_SECTION,
|
||||||
|
'trash')
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.vprint('{info} Attempting to connect to nessus...'.format(info=bcolors.INFO))
|
||||||
|
self.nessus = \
|
||||||
|
NessusAPI(hostname=self.hostname,
|
||||||
|
port=self.nessus_port,
|
||||||
|
username=self.username,
|
||||||
|
password=self.password)
|
||||||
|
self.nessus_connect = True
|
||||||
|
self.vprint('{success} Connected to nessus on {host}:{port}'.format(success=bcolors.SUCCESS,
|
||||||
|
host=self.hostname,
|
||||||
|
port=str(self.nessus_port)))
|
||||||
|
except Exception as e:
|
||||||
|
self.vprint(e)
|
||||||
|
raise Exception(
|
||||||
|
'{fail} Could not connect to nessus -- Please verify your settings in {config} are correct and try again.\nReason: {e}'.format(
|
||||||
|
config=self.config,
|
||||||
|
fail=bcolors.FAIL, e=e))
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
self.vprint('{fail} Could not properly load your config!\nReason: {e}'.format(fail=bcolors.FAIL,
|
||||||
|
e=e))
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def scan_count(self, scans, completed=False):
|
def scan_count(self, scans, completed=False):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -206,32 +235,15 @@ class vulnWhisperer(object):
|
|||||||
]))
|
]))
|
||||||
scan_records.append(record.copy())
|
scan_records.append(record.copy())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Generates error each time nonetype is encountered.
|
||||||
# print(e)
|
# print(e)
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if completed:
|
if completed:
|
||||||
scan_records = [s for s in scan_records if s['status']
|
scan_records = [s for s in scan_records if s['status'] == 'completed']
|
||||||
== 'completed']
|
|
||||||
return scan_records
|
return scan_records
|
||||||
|
|
||||||
def record_insert(self, record):
|
|
||||||
self.cur.execute('insert into scan_history({table_columns}) values (?,?,?,?,?,?,?,?,?)'.format(
|
|
||||||
table_columns=', '.join(self.table_columns)),
|
|
||||||
record)
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
def retrieve_uuids(self):
|
|
||||||
"""
|
|
||||||
Retrieves UUIDs from database and checks list to determine which files need to be processed.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.conn.text_factory = str
|
|
||||||
self.cur.execute('SELECT uuid FROM scan_history')
|
|
||||||
results = frozenset([r[0] for r in self.cur.fetchall()])
|
|
||||||
return results
|
|
||||||
|
|
||||||
def whisper_nessus(self):
|
def whisper_nessus(self):
|
||||||
if self.nessus_connect:
|
if self.nessus_connect:
|
||||||
@ -299,12 +311,9 @@ class vulnWhisperer(object):
|
|||||||
if status == 'completed':
|
if status == 'completed':
|
||||||
file_name = '%s_%s_%s_%s.%s' % (scan_name, scan_id,
|
file_name = '%s_%s_%s_%s.%s' % (scan_name, scan_id,
|
||||||
history_id, norm_time, 'csv')
|
history_id, norm_time, 'csv')
|
||||||
repls = (('\\', '_'), ('/', '_'), ('/', '_'), (' ',
|
repls = (('\\', '_'), ('/', '_'), ('/', '_'), (' ', '_'))
|
||||||
'_'))
|
file_name = reduce(lambda a, kv: a.replace(*kv), repls, file_name)
|
||||||
file_name = reduce(lambda a, kv: a.replace(*kv),
|
relative_path_name = self.path_check(folder_name + '/' + file_name)
|
||||||
repls, file_name)
|
|
||||||
relative_path_name = self.path_check(folder_name
|
|
||||||
+ '/' + file_name)
|
|
||||||
|
|
||||||
if os.path.isfile(relative_path_name):
|
if os.path.isfile(relative_path_name):
|
||||||
if self.develop:
|
if self.develop:
|
||||||
@ -335,23 +344,14 @@ class vulnWhisperer(object):
|
|||||||
self.vprint('Processing %s/%s for scan: %s'
|
self.vprint('Processing %s/%s for scan: %s'
|
||||||
% (scan_count, len(scan_history),
|
% (scan_count, len(scan_history),
|
||||||
scan_name))
|
scan_name))
|
||||||
clean_csv['CVSS'] = clean_csv['CVSS'
|
columns_to_cleanse = ['CVSS','CVE','Description','Synopsis','Solution','See Also','Plugin Output']
|
||||||
].astype(str).apply(self.cleanser)
|
|
||||||
clean_csv['CVE'] = clean_csv['CVE'
|
for col in columns_to_cleanse:
|
||||||
].astype(str).apply(self.cleanser)
|
clean_csv[col] = clean_csv[col].astype(str).apply(self.cleanser)
|
||||||
clean_csv['Description'] = \
|
|
||||||
clean_csv['Description'
|
|
||||||
].astype(str).apply(self.cleanser)
|
|
||||||
clean_csv['Synopsis'] = \
|
clean_csv['Synopsis'] = \
|
||||||
clean_csv['Description'
|
clean_csv['Description'
|
||||||
].astype(str).apply(self.cleanser)
|
].astype(str).apply(self.cleanser)
|
||||||
clean_csv['Solution'] = clean_csv['Solution'
|
|
||||||
].astype(str).apply(self.cleanser)
|
|
||||||
clean_csv['See Also'] = clean_csv['See Also'
|
|
||||||
].astype(str).apply(self.cleanser)
|
|
||||||
clean_csv['Plugin Output'] = \
|
|
||||||
clean_csv['Plugin Output'
|
|
||||||
].astype(str).apply(self.cleanser)
|
|
||||||
clean_csv.to_csv(relative_path_name,
|
clean_csv.to_csv(relative_path_name,
|
||||||
index=False)
|
index=False)
|
||||||
record_meta = (
|
record_meta = (
|
||||||
@ -391,8 +391,129 @@ class vulnWhisperer(object):
|
|||||||
else:
|
else:
|
||||||
|
|
||||||
self.vprint('{fail} Failed to use scanner at {host}'.format(fail=bcolors.FAIL,
|
self.vprint('{fail} Failed to use scanner at {host}'.format(fail=bcolors.FAIL,
|
||||||
host=self.nessus_hostname + ':'
|
host=self.hostname + ':'
|
||||||
+ self.nessus_port))
|
+ self.nessus_port))
|
||||||
|
|
||||||
|
|
||||||
|
class vulnWhispererQualys(vulnWhispererBase):
|
||||||
|
|
||||||
|
CONFIG_SECTION = 'qualys'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config=None,
|
||||||
|
db_name='report_tracker.db',
|
||||||
|
purge=False,
|
||||||
|
verbose=None,
|
||||||
|
debug=False,
|
||||||
|
username=None,
|
||||||
|
password=None,
|
||||||
|
):
|
||||||
|
super(vulnWhispererQualys, self).__init__(config=config, )
|
||||||
|
|
||||||
|
self.qualys_web = qualysWebAppReport(config=config)
|
||||||
|
self.latest_scans = self.qualys_web.qw.get_web_app_list()
|
||||||
|
|
||||||
|
|
||||||
|
def whisper_webapp(self, report_id, updated_date):
|
||||||
|
"""
|
||||||
|
report_id: App ID
|
||||||
|
updated_date: Last time scan was ran for app_id
|
||||||
|
"""
|
||||||
|
vuln_ready = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if 'Z' in updated_date:
|
||||||
|
updated_date = self.qualys_web.iso_to_epoch(updated_date)
|
||||||
|
report_name = 'qualys_web_' + str(report_id) \
|
||||||
|
+ '_{last_updated}'.format(last_updated=updated_date) \
|
||||||
|
+ '.csv'
|
||||||
|
if os.path.isfile(report_name):
|
||||||
|
print('{action} - File already exist! Skipping...'.format(action=bcolors.ACTION))
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
print('{action} - Generating report for %s'.format(action=bcolors.ACTION) % report_id)
|
||||||
|
status = self.qualys_web.qw.create_report(report_id)
|
||||||
|
root = objectify.fromstring(status)
|
||||||
|
if root.responseCode == 'SUCCESS':
|
||||||
|
print('{info} - Successfully generated report for webapp: %s'.format(info=bcolors.INFO) \
|
||||||
|
% report_id)
|
||||||
|
generated_report_id = root.data.Report.id
|
||||||
|
print('{info} - New Report ID: %s'.format(info=bcolors.INFO) \
|
||||||
|
% generated_report_id)
|
||||||
|
vuln_ready = self.qualys_web.process_data(generated_report_id)
|
||||||
|
|
||||||
|
vuln_ready.to_csv(report_name, index=False, header=True) # add when timestamp occured
|
||||||
|
print('{success} - Report written to %s'.format(success=bcolors.SUCCESS) \
|
||||||
|
% report_name)
|
||||||
|
print('{action} - Removing report %s'.format(action=bcolors.ACTION) \
|
||||||
|
% generated_report_id)
|
||||||
|
cleaning_up = \
|
||||||
|
self.qualys_web.qw.delete_report(generated_report_id)
|
||||||
|
os.remove(str(generated_report_id) + '.csv')
|
||||||
|
print('{action} - Deleted report: %s'.format(action=bcolors.ACTION) \
|
||||||
|
% generated_report_id)
|
||||||
|
else:
|
||||||
|
print('{error} Could not process report ID: %s'.format(error=bcolors.FAIL) % status)
|
||||||
|
except Exception as e:
|
||||||
|
print('{error} - Could not process %s - %s'.format(error=bcolors.FAIL) % (report_id, e))
|
||||||
|
return vuln_ready
|
||||||
|
|
||||||
|
def process_web_assets(self):
|
||||||
|
counter = 0
|
||||||
|
for app in self.latest_scans.iterrows():
|
||||||
|
counter += 1
|
||||||
|
print('Processing %s/%s' % (counter, len(self.latest_scans)))
|
||||||
|
self.whisper_webapp(app[1]['id'], app[1]['createdDate'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class vulnWhisperer(object):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
profile=None,
|
||||||
|
verbose=None,
|
||||||
|
username=None,
|
||||||
|
password=None,
|
||||||
|
config=None):
|
||||||
|
|
||||||
|
self.profile = profile
|
||||||
|
self.config = config
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.verbose = verbose
|
||||||
|
|
||||||
|
|
||||||
|
def whisper_vulnerabilities(self):
|
||||||
|
|
||||||
|
if self.profile == 'nessus':
|
||||||
|
vw = vulnWhispererNessus(config=self.config, username=self.username, password=self.password, verbose=self.verbose)
|
||||||
|
vw.whisper_nessus()
|
||||||
|
|
||||||
|
elif self.profile == 'qualys':
|
||||||
|
vw = vulnWhispererQualys(config=self.config)
|
||||||
|
vw.process_web_assets()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
for f in folders:
|
||||||
|
if not os.path.exists(self.path_check(f['name'])):
|
||||||
|
if f['name'] == 'Trash' and self.nessus_trash:
|
||||||
|
os.makedirs(self.path_check(f['name']))
|
||||||
|
elif f['name'] != 'Trash':
|
||||||
|
os.makedirs(self.path_check(f['name']))
|
||||||
|
else:
|
||||||
|
os.path.exists(self.path_check(f['name']))
|
||||||
|
self.vprint('{info} Directory already exist for {scan} - Skipping creation'.format(
|
||||||
|
scan=self.path_check(f['name'
|
||||||
|
]), info=bcolors.INFO))
|
||||||
|
|
||||||
|
'''
|
Reference in New Issue
Block a user