11 Commits

Author SHA1 Message Date
1a3165cfc6 Bump lxml from 4.6.5 to 4.9.1
Bumps [lxml](https://github.com/lxml/lxml) from 4.6.5 to 4.9.1.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.5...lxml-4.9.1)

---
updated-dependencies:
- dependency-name: lxml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-06 21:50:09 +00:00
691f45a1dc Merge pull request #232 from HASecuritySolutions/dependabot/pip/lxml-4.6.5
Bump lxml from 4.1.1 to 4.6.5
2022-06-11 20:39:14 -05:00
80197454a3 Update README.md 2022-02-03 10:33:12 -06:00
841cd09f2d Bump lxml from 4.1.1 to 4.6.5
Bumps [lxml](https://github.com/lxml/lxml) from 4.1.1 to 4.6.5.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.1.1...lxml-4.6.5)

---
updated-dependencies:
- dependency-name: lxml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-13 19:44:12 +00:00
e7183864d0 Merge pull request #216 from Yashvendra/patch-1
Updated 3000_openvas.conf
2020-07-20 10:45:00 +02:00
12ac3dbf62 Merge pull request #217 from andrew-bailey/patch-1
Update README.md
2020-07-20 10:43:09 +02:00
e41ec93058 Update README.md
Fix license badge from MIT to Apache 2.0 which is the current license applied in Github
2020-07-20 11:57:36 +09:30
8a86e3142a Update 3000_openvas.conf
Fixed Description
2020-07-19 14:41:21 +05:30
9d003d12b4 improved error logging and excepcions 2020-04-08 12:01:47 +02:00
63c638751b Merge pull request #207 from spasaintk/patch-1
Update vulnwhisp.py
2020-02-29 20:05:51 +01:00
a3e85b7207 Update vulnwhisp.py
Code triggers a crash:
ERROR:root:main:local variable 'vw' referenced before assignment
ERROR: local variable 'vw' referenced before assignment

Proposed fix deals with the issue.
After fix:
INFO:vulnWhispererOpenVAS:process_openvas_scans:Processing complete
2020-02-28 00:33:38 +01:00
14 changed files with 1759 additions and 1747 deletions

View File

@ -6,8 +6,10 @@
VulnWhisperer is a vulnerability management tool and report aggregator. VulnWhisperer will pull all the reports from the different Vulnerability scanners and create a file with a unique filename for each one, using that data later to sync with Jira and feed Logstash. Jira does a closed cycle full Sync with the data provided by the Scanners, while Logstash indexes and tags all of the information inside the report (see logstash files at /resources/elk6/pipeline/). Data is then shipped to ElasticSearch to be indexed, and ends up in a visual and searchable format in Kibana with already defined dashboards. VulnWhisperer is a vulnerability management tool and report aggregator. VulnWhisperer will pull all the reports from the different Vulnerability scanners and create a file with a unique filename for each one, using that data later to sync with Jira and feed Logstash. Jira does a closed cycle full Sync with the data provided by the Scanners, while Logstash indexes and tags all of the information inside the report (see logstash files at /resources/elk6/pipeline/). Data is then shipped to ElasticSearch to be indexed, and ends up in a visual and searchable format in Kibana with already defined dashboards.
VulnWhisperer is an open-source community funded project. VulnWhisperer currently works but is due for a documentation overhaul and code review. This is on the roadmap for the next month or two (February or March of 2022 - hopefully). Please note, crowd funding is an option. If you would like help getting VulnWhisperer up and running, are interested in new features, or are looking for paid support (for those of you that require commercial support contracts to implement open-source solutions), please reach out to **info@hasecuritysolutions.com**.
[![Build Status](https://travis-ci.org/HASecuritySolutions/VulnWhisperer.svg?branch=master)](https://travis-ci.org/HASecuritySolutions/VulnWhisperer) [![Build Status](https://travis-ci.org/HASecuritySolutions/VulnWhisperer.svg?branch=master)](https://travis-ci.org/HASecuritySolutions/VulnWhisperer)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://choosealicense.com/licenses/mit/) [![GitHub license](https://img.shields.io/github/license/HASecuritySolutions/VulnWhisperer)](https://github.com/HASecuritySolutions/VulnWhisperer/blob/master/LICENSE)
[![Twitter](https://img.shields.io/twitter/follow/VulnWhisperer.svg?style=social&label=Follow)](https://twitter.com/VulnWhisperer) [![Twitter](https://img.shields.io/twitter/follow/VulnWhisperer.svg?style=social&label=Follow)](https://twitter.com/VulnWhisperer)
Currently Supports Currently Supports
@ -30,7 +32,8 @@ Currently Supports
### Reporting Frameworks ### Reporting Frameworks
- [X] [ELK (**v6**/**v7**)](https://www.elastic.co/elk-stack) - [X] [Elastic Stack (**v6**/**v7**)](https://www.elastic.co/elk-stack)
- [ ] [OpenSearch - Being considered for next update](https://opensearch.org/)
- [X] [Jira](https://www.atlassian.com/software/jira) - [X] [Jira](https://www.atlassian.com/software/jira)
- [ ] [Splunk](https://www.splunk.com/) - [ ] [Splunk](https://www.splunk.com/)

View File

@ -93,7 +93,7 @@ def main():
scanname=args.scanname) scanname=args.scanname)
exit_code += vw.whisper_vulnerabilities() exit_code += vw.whisper_vulnerabilities()
except Exception as e: except Exception as e:
logger.error("VulnWhisperer was unable to perform the processing on '{}'".format(section)) logger.error("VulnWhisperer was unable to perform the processing on '{}'".format(args.source))
else: else:
logger.info('Running vulnwhisperer for section {}'.format(args.section)) logger.info('Running vulnwhisperer for section {}'.format(args.section))
vw = vulnWhisperer(config=args.config, vw = vulnWhisperer(config=args.config,

View File

@ -6,8 +6,8 @@ access_key=
secret_key= secret_key=
username=nessus_username username=nessus_username
password=nessus_password password=nessus_password
write_path=/tmp/VulnWhisperer/data/nessus/ write_path=/opt/VulnWhisperer/data/nessus/
db_path=/tmp/VulnWhisperer/data/database db_path=/opt/VulnWhisperer/data/database
trash=false trash=false
verbose=true verbose=true
@ -19,8 +19,8 @@ access_key=
secret_key= secret_key=
username=tenable.io_username username=tenable.io_username
password=tenable.io_password password=tenable.io_password
write_path=/tmp/VulnWhisperer/data/tenable/ write_path=/opt/VulnWhisperer/data/tenable/
db_path=/tmp/VulnWhisperer/data/database db_path=/opt/VulnWhisperer/data/database
trash=false trash=false
verbose=true verbose=true
@ -30,8 +30,8 @@ enabled = false
hostname = qualys_web hostname = qualys_web
username = exampleuser username = exampleuser
password = examplepass password = examplepass
write_path=/tmp/VulnWhisperer/data/qualys_web/ write_path=/opt/VulnWhisperer/data/qualys_web/
db_path=/tmp/VulnWhisperer/data/database db_path=/opt/VulnWhisperer/data/database
verbose=true verbose=true
# Set the maximum number of retries each connection should attempt. # Set the maximum number of retries each connection should attempt.
@ -46,8 +46,8 @@ enabled = true
hostname = qualys_vuln hostname = qualys_vuln
username = exampleuser username = exampleuser
password = examplepass password = examplepass
write_path=/tmp/VulnWhisperer/data/qualys_vuln/ write_path=/opt/VulnWhisperer/data/qualys_vuln/
db_path=/tmp/VulnWhisperer/data/database db_path=/opt/VulnWhisperer/data/database
verbose=true verbose=true
[detectify] [detectify]
@ -58,8 +58,8 @@ hostname = detectify
username = exampleuser username = exampleuser
#password variable used as secretKey #password variable used as secretKey
password = examplepass password = examplepass
write_path =/tmp/VulnWhisperer/data/detectify/ write_path =/opt/VulnWhisperer/data/detectify/
db_path = /tmp/VulnWhisperer/data/database db_path = /opt/VulnWhisperer/data/database
verbose = true verbose = true
[openvas] [openvas]
@ -68,8 +68,8 @@ hostname = openvas
port = 4000 port = 4000
username = exampleuser username = exampleuser
password = examplepass password = examplepass
write_path=/tmp/VulnWhisperer/data/openvas/ write_path=/opt/VulnWhisperer/data/openvas/
db_path=/tmp/VulnWhisperer/data/database db_path=/opt/VulnWhisperer/data/database
verbose=true verbose=true
[jira] [jira]
@ -77,8 +77,8 @@ enabled = false
hostname = jira-host hostname = jira-host
username = username username = username
password = password password = password
write_path = /tmp/VulnWhisperer/data/jira/ write_path = /opt/VulnWhisperer/data/jira/
db_path = /tmp/VulnWhisperer/data/database db_path = /opt/VulnWhisperer/data/database
verbose = true verbose = true
dns_resolv = False dns_resolv = False

View File

@ -2,7 +2,7 @@ pandas==0.20.3
setuptools==40.4.3 setuptools==40.4.3
pytz==2017.2 pytz==2017.2
Requests==2.20.0 Requests==2.20.0
lxml==4.1.1 lxml==4.9.1
future-fstrings future-fstrings
bs4 bs4
jira jira

View File

@ -2,7 +2,7 @@
# Email: austin@hasecuritysolutions.com # Email: austin@hasecuritysolutions.com
# Last Update: 03/04/2018 # Last Update: 03/04/2018
# Version 0.3 # Version 0.3
# Description: Take in qualys web scan reports from vulnWhisperer and pumps into logstash # Description: Take in Openvas web scan reports from vulnWhisperer and pumps into logstash
input { input {
file { file {

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import absolute_import
from setuptools import setup, find_packages from setuptools import setup, find_packages
setup( setup(

View File

@ -1,4 +1,3 @@
from __future__ import absolute_import
import sys import sys
import logging import logging
@ -6,7 +5,7 @@ import logging
if sys.version_info > (3, 0): if sys.version_info > (3, 0):
import configparser as cp import configparser as cp
else: else:
import six.moves.configparser as cp import ConfigParser as cp
class vwConfig(object): class vwConfig(object):

View File

@ -1,4 +1,3 @@
from __future__ import absolute_import
import json import json
import logging import logging
import sys import sys

View File

@ -1,6 +1,5 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import
__author__ = 'Austin Taylor' __author__ = 'Austin Taylor'
import datetime as dt import datetime as dt

View File

@ -1,6 +1,5 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import
__author__ = 'Nathan Young' __author__ = 'Nathan Young'
import logging import logging
@ -19,9 +18,9 @@ class qualysWhisperAPI(object):
self.logger = logging.getLogger('qualysWhisperAPI') self.logger = logging.getLogger('qualysWhisperAPI')
self.config = config self.config = config
try: try:
self.qgc = qualysapi.connect(config_file=config, section='qualys_vuln') self.qgc = qualysapi.connect(config, 'qualys_vuln')
# Fail early if we can't make a request or auth is incorrect # Fail early if we can't make a request or auth is incorrect
# self.qgc.request('about.php') self.qgc.request('about.php')
self.logger.info('Connected to Qualys at {}'.format(self.qgc.server)) self.logger.info('Connected to Qualys at {}'.format(self.qgc.server))
except Exception as e: except Exception as e:
self.logger.error('Could not connect to Qualys: {}'.format(str(e))) self.logger.error('Could not connect to Qualys: {}'.format(str(e)))

View File

@ -1,8 +1,5 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import
from six.moves import range
from functools import reduce
__author__ = 'Austin Taylor' __author__ = 'Austin Taylor'
from lxml import objectify from lxml import objectify
@ -17,16 +14,24 @@ import os
import csv import csv
import logging import logging
import dateutil.parser as dp import dateutil.parser as dp
csv.field_size_limit(sys.maxsize)
class qualysWhisperAPI(object): class qualysWhisperAPI(object):
COUNT_WEBAPP = '/count/was/webapp'
COUNT_WASSCAN = '/count/was/wasscan' COUNT_WASSCAN = '/count/was/wasscan'
DELETE_REPORT = '/delete/was/report/{report_id}' DELETE_REPORT = '/delete/was/report/{report_id}'
GET_WEBAPP_DETAILS = '/get/was/webapp/{was_id}'
QPS_REST_3 = '/qps/rest/3.0'
REPORT_DETAILS = '/get/was/report/{report_id}'
REPORT_STATUS = '/status/was/report/{report_id}' REPORT_STATUS = '/status/was/report/{report_id}'
REPORT_CREATE = '/create/was/report' REPORT_CREATE = '/create/was/report'
REPORT_DOWNLOAD = '/download/was/report/{report_id}' REPORT_DOWNLOAD = '/download/was/report/{report_id}'
SCAN_DETAILS = '/get/was/wasscan/{scan_id}'
SCAN_DOWNLOAD = '/download/was/wasscan/{scan_id}'
SEARCH_REPORTS = '/search/was/report'
SEARCH_WEB_APPS = '/search/was/webapp'
SEARCH_WAS_SCAN = '/search/was/wasscan' SEARCH_WAS_SCAN = '/search/was/wasscan'
VERSION = '/qps/rest/portal/version'
def __init__(self, config=None): def __init__(self, config=None):
self.logger = logging.getLogger('qualysWhisperAPI') self.logger = logging.getLogger('qualysWhisperAPI')
@ -36,6 +41,10 @@ class qualysWhisperAPI(object):
self.logger.info('Connected to Qualys at {}'.format(self.qgc.server)) self.logger.info('Connected to Qualys at {}'.format(self.qgc.server))
except Exception as e: except Exception as e:
self.logger.error('Could not connect to Qualys: {}'.format(str(e))) self.logger.error('Could not connect to Qualys: {}'.format(str(e)))
self.headers = {
#"content-type": "text/xml"}
"Accept" : "application/json",
"Content-Type": "application/json"}
self.config_parse = qcconf.QualysConnectConfig(config, 'qualys_web') self.config_parse = qcconf.QualysConnectConfig(config, 'qualys_web')
try: try:
self.template_id = self.config_parse.get_template_id() self.template_id = self.config_parse.get_template_id()
@ -60,8 +69,14 @@ class qualysWhisperAPI(object):
def generate_scan_result_XML(self, limit=1000, offset=1, status='FINISHED'): def generate_scan_result_XML(self, limit=1000, offset=1, status='FINISHED'):
report_xml = E.ServiceRequest( report_xml = E.ServiceRequest(
E.filters(E.Criteria({'field': 'status', 'operator': 'EQUALS'}, status)), E.filters(
E.preferences(E.startFromOffset(str(offset)), E.limitResults(str(limit))), E.Criteria({'field': 'status', 'operator': 'EQUALS'}, status
),
),
E.preferences(
E.startFromOffset(str(offset)),
E.limitResults(str(limit))
),
) )
return report_xml return report_xml
@ -100,10 +115,8 @@ class qualysWhisperAPI(object):
if i % limit == 0: if i % limit == 0:
if (total - i) < limit: if (total - i) < limit:
qualys_api_limit = total - i qualys_api_limit = total - i
self.logger.info('Making a request with a limit of {} at offset {}' self.logger.info('Making a request with a limit of {} at offset {}'.format((str(qualys_api_limit)), str(i + 1)))
.format((str(qualys_api_limit)), str(i + 1))) scan_info = self.get_scan_info(limit=qualys_api_limit, offset=i + 1, status=status)
scan_info = self.get_scan_info(
limit=qualys_api_limit, offset=i + 1, status=status)
_records.append(scan_info) _records.append(scan_info)
self.logger.debug('Converting XML to DataFrame') self.logger.debug('Converting XML to DataFrame')
dataframes = [self.xml_parser(xml) for xml in _records] dataframes = [self.xml_parser(xml) for xml in _records]
@ -120,8 +133,7 @@ class qualysWhisperAPI(object):
return self.qgc.request(self.REPORT_STATUS.format(report_id=report_id)) return self.qgc.request(self.REPORT_STATUS.format(report_id=report_id))
def download_report(self, report_id): def download_report(self, report_id):
return self.qgc.request( return self.qgc.request(self.REPORT_DOWNLOAD.format(report_id=report_id))
self.REPORT_DOWNLOAD.format(report_id=report_id), http_method='get')
def generate_scan_report_XML(self, scan_id): def generate_scan_report_XML(self, scan_id):
"""Generates a CSV report for an asset based on template defined in .ini file""" """Generates a CSV report for an asset based on template defined in .ini file"""
@ -133,8 +145,20 @@ class qualysWhisperAPI(object):
E.format('CSV'), E.format('CSV'),
#type is not needed, as the template already has it #type is not needed, as the template already has it
E.type('WAS_SCAN_REPORT'), E.type('WAS_SCAN_REPORT'),
E.template(E.id(self.template_id)), E.template(
E.config(E.scanReport(E.target(E.scans(E.WasScan(E.id(scan_id)))))) E.id(self.template_id)
),
E.config(
E.scanReport(
E.target(
E.scans(
E.WasScan(
E.id(scan_id)
)
),
),
),
)
) )
) )
) )
@ -151,14 +175,95 @@ class qualysWhisperAPI(object):
def delete_report(self, report_id): def delete_report(self, report_id):
return self.qgc.request(self.DELETE_REPORT.format(report_id=report_id)) return self.qgc.request(self.DELETE_REPORT.format(report_id=report_id))
class qualysReportFields:
CATEGORIES = ['VULNERABILITY',
'SENSITIVECONTENT',
'INFORMATION_GATHERED']
# URL Vulnerability Information
VULN_BLOCK = [
CATEGORIES[0],
'ID',
'QID',
'Url',
'Param',
'Function',
'Form Entry Point',
'Access Path',
'Authentication',
'Ajax Request',
'Ajax Request ID',
'Ignored',
'Ignore Reason',
'Ignore Date',
'Ignore User',
'Ignore Comments',
'First Time Detected',
'Last Time Detected',
'Last Time Tested',
'Times Detected',
'Payload #1',
'Request Method #1',
'Request URL #1',
'Request Headers #1',
'Response #1',
'Evidence #1',
]
INFO_HEADER = [
'Vulnerability Category',
'ID',
'QID',
'Response #1',
'Last Time Detected',
]
INFO_BLOCK = [
CATEGORIES[2],
'ID',
'QID',
'Results',
'Detection Date',
]
QID_HEADER = [
'QID',
'Id',
'Title',
'Category',
'Severity Level',
'Groups',
'OWASP',
'WASC',
'CWE',
'CVSS Base',
'CVSS Temporal',
'Description',
'Impact',
'Solution',
]
GROUP_HEADER = ['GROUP', 'Name', 'Category']
OWASP_HEADER = ['OWASP', 'Code', 'Name']
WASC_HEADER = ['WASC', 'Code', 'Name']
SCAN_META = ['Web Application Name', 'URL', 'Owner', 'Scope', 'Operating System']
CATEGORY_HEADER = ['Category', 'Severity', 'Level', 'Description']
class qualysUtils: class qualysUtils:
def __init__(self): def __init__(self):
self.logger = logging.getLogger('qualysUtils') self.logger = logging.getLogger('qualysUtils')
def grab_section(self, report, section, end=[], pop_last=False): def grab_section(
self,
report,
section,
end=[],
pop_last=False,
):
temp_list = [] temp_list = []
max_col_count = 0 max_col_count = 0
with open(report, 'rt') as csvfile: with open(report, 'rb') as csvfile:
q_report = csv.reader(csvfile, delimiter=',', quotechar='"') q_report = csv.reader(csvfile, delimiter=',', quotechar='"')
for line in q_report: for line in q_report:
if set(line) == set(section): if set(line) == set(section):
@ -184,53 +289,44 @@ class qualysUtils:
return _data return _data
class qualysScanReport: class qualysScanReport:
CATEGORIES = ['VULNERABILITY', 'SENSITIVECONTENT', 'INFORMATION_GATHERED'] # URL Vulnerability Information
WEB_SCAN_VULN_BLOCK = list(qualysReportFields.VULN_BLOCK)
WEB_SCAN_VULN_BLOCK.insert(WEB_SCAN_VULN_BLOCK.index('QID'), 'Detection ID')
WEB_SCAN_BLOCK = [ WEB_SCAN_VULN_HEADER = list(WEB_SCAN_VULN_BLOCK)
"ID", "Detection ID", "QID", "Url", "Param/Cookie", "Function", WEB_SCAN_VULN_HEADER[WEB_SCAN_VULN_BLOCK.index(qualysReportFields.CATEGORIES[0])] = \
"Form Entry Point", "Access Path", "Authentication", "Ajax Request", 'Vulnerability Category'
"Ajax Request ID", "Ignored", "Ignore Reason", "Ignore Date", "Ignore User",
"Ignore Comments", "Detection Date", "Payload #1", "Request Method #1",
"Request URL #1", "Request Headers #1", "Response #1", "Evidence #1",
"Unique ID", "Flags", "Protocol", "Virtual Host", "IP", "Port", "Result",
"Info#1", "CVSS V3 Base", "CVSS V3 Temporal", "CVSS V3 Attack Vector",
"Request Body #1"
]
WEB_SCAN_VULN_BLOCK = [CATEGORIES[0]] + WEB_SCAN_BLOCK
WEB_SCAN_SENSITIVE_BLOCK = [CATEGORIES[1]] + WEB_SCAN_BLOCK
WEB_SCAN_HEADER = ["Vulnerability Category"] + WEB_SCAN_BLOCK WEB_SCAN_SENSITIVE_HEADER = list(WEB_SCAN_VULN_HEADER)
WEB_SCAN_HEADER[WEB_SCAN_HEADER.index("Detection Date")] = "Last Time Detected" WEB_SCAN_SENSITIVE_HEADER.insert(WEB_SCAN_SENSITIVE_HEADER.index('Url'
), 'Content')
WEB_SCAN_SENSITIVE_BLOCK = list(WEB_SCAN_SENSITIVE_HEADER)
WEB_SCAN_SENSITIVE_BLOCK.insert(WEB_SCAN_SENSITIVE_BLOCK.index('QID'), 'Detection ID')
WEB_SCAN_SENSITIVE_BLOCK[WEB_SCAN_SENSITIVE_BLOCK.index('Vulnerability Category'
)] = qualysReportFields.CATEGORIES[1]
WEB_SCAN_INFO_BLOCK = [ WEB_SCAN_INFO_HEADER = list(qualysReportFields.INFO_HEADER)
"INFORMATION_GATHERED", "ID", "Detection ID", "QID", "Results", "Detection Date", WEB_SCAN_INFO_HEADER.insert(WEB_SCAN_INFO_HEADER.index('QID'), 'Detection ID')
"Unique ID", "Flags", "Protocol", "Virtual Host", "IP", "Port", "Result",
"Info#1"
]
WEB_SCAN_INFO_HEADER = [ WEB_SCAN_INFO_BLOCK = list(qualysReportFields.INFO_BLOCK)
"Vulnerability Category", "ID", "Detection ID", "QID", "Results", "Last Time Detected", WEB_SCAN_INFO_BLOCK.insert(WEB_SCAN_INFO_BLOCK.index('QID'), 'Detection ID')
"Unique ID", "Flags", "Protocol", "Virtual Host", "IP", "Port", "Result",
"Info#1"
]
QID_HEADER = [ QID_HEADER = list(qualysReportFields.QID_HEADER)
"QID", "Id", "Title", "Category", "Severity Level", "Groups", "OWASP", "WASC", GROUP_HEADER = list(qualysReportFields.GROUP_HEADER)
"CWE", "CVSS Base", "CVSS Temporal", "Description", "Impact", "Solution", OWASP_HEADER = list(qualysReportFields.OWASP_HEADER)
"CVSS V3 Base", "CVSS V3 Temporal", "CVSS V3 Attack Vector" WASC_HEADER = list(qualysReportFields.WASC_HEADER)
] SCAN_META = list(qualysReportFields.SCAN_META)
GROUP_HEADER = ['GROUP', 'Name', 'Category'] CATEGORY_HEADER = list(qualysReportFields.CATEGORY_HEADER)
OWASP_HEADER = ['OWASP', 'Code', 'Name']
WASC_HEADER = ['WASC', 'Code', 'Name']
SCAN_META = [
"Web Application Name", "URL", "Owner", "Scope", "ID", "Tags",
"Custom Attributes"
]
CATEGORY_HEADER = ['Category', 'Severity', 'Level', 'Description']
def __init__(self, config=None, file_in=None, def __init__(
file_stream=False, delimiter=',', quotechar='"'): self,
config=None,
file_in=None,
file_stream=False,
delimiter=',',
quotechar='"',
):
self.logger = logging.getLogger('qualysScanReport') self.logger = logging.getLogger('qualysScanReport')
self.file_in = file_in self.file_in = file_in
self.file_stream = file_stream self.file_stream = file_stream
@ -241,79 +337,71 @@ class qualysScanReport:
try: try:
self.qw = qualysWhisperAPI(config=config) self.qw = qualysWhisperAPI(config=config)
except Exception as e: except Exception as e:
self.logger.error( self.logger.error('Could not load config! Please check settings. Error: {}'.format(str(e)))
'Could not load config! Please check settings. Error: {}'.format(
str(e)))
if file_stream: if file_stream:
self.open_file = file_in.splitlines() self.open_file = file_in.splitlines()
elif file_in: elif file_in:
self.open_file = open(file_in, 'rb') self.open_file = open(file_in, 'rb')
self.downloaded_file = None self.downloaded_file = None
def grab_sections(self, report): def grab_sections(self, report):
return { all_dataframes = []
'WEB_SCAN_VULN_BLOCK': pd.DataFrame( dict_tracker = {}
self.utils.grab_section( with open(report, 'rb') as csvfile:
report, dict_tracker['WEB_SCAN_VULN_BLOCK'] = pd.DataFrame(self.utils.grab_section(report,
self.WEB_SCAN_VULN_BLOCK, self.WEB_SCAN_VULN_BLOCK,
end=[self.WEB_SCAN_SENSITIVE_BLOCK, self.WEB_SCAN_INFO_BLOCK], end=[
pop_last=True), self.WEB_SCAN_SENSITIVE_BLOCK,
columns=self.WEB_SCAN_HEADER), self.WEB_SCAN_INFO_BLOCK],
'WEB_SCAN_SENSITIVE_BLOCK': pd.DataFrame( pop_last=True),
self.utils.grab_section(report, columns=self.WEB_SCAN_VULN_HEADER)
self.WEB_SCAN_SENSITIVE_BLOCK, dict_tracker['WEB_SCAN_SENSITIVE_BLOCK'] = pd.DataFrame(self.utils.grab_section(report,
end=[self.WEB_SCAN_INFO_BLOCK, self.WEB_SCAN_SENSITIVE_BLOCK], self.WEB_SCAN_SENSITIVE_BLOCK,
pop_last=True), end=[
columns=self.WEB_SCAN_HEADER), self.WEB_SCAN_INFO_BLOCK,
'WEB_SCAN_INFO_BLOCK': pd.DataFrame( self.WEB_SCAN_SENSITIVE_BLOCK],
self.utils.grab_section( pop_last=True),
report, columns=self.WEB_SCAN_SENSITIVE_HEADER)
self.WEB_SCAN_INFO_BLOCK, dict_tracker['WEB_SCAN_INFO_BLOCK'] = pd.DataFrame(self.utils.grab_section(report,
end=[self.QID_HEADER], self.WEB_SCAN_INFO_BLOCK,
pop_last=True), end=[self.QID_HEADER],
columns=self.WEB_SCAN_INFO_HEADER), pop_last=True),
columns=self.WEB_SCAN_INFO_HEADER)
dict_tracker['QID_HEADER'] = pd.DataFrame(self.utils.grab_section(report,
self.QID_HEADER,
end=[self.GROUP_HEADER],
pop_last=True),
columns=self.QID_HEADER)
dict_tracker['GROUP_HEADER'] = pd.DataFrame(self.utils.grab_section(report,
self.GROUP_HEADER,
end=[self.OWASP_HEADER],
pop_last=True),
columns=self.GROUP_HEADER)
dict_tracker['OWASP_HEADER'] = pd.DataFrame(self.utils.grab_section(report,
self.OWASP_HEADER,
end=[self.WASC_HEADER],
pop_last=True),
columns=self.OWASP_HEADER)
dict_tracker['WASC_HEADER'] = pd.DataFrame(self.utils.grab_section(report,
self.WASC_HEADER, end=[['APPENDIX']],
pop_last=True),
columns=self.WASC_HEADER)
'QID_HEADER': pd.DataFrame( dict_tracker['SCAN_META'] = pd.DataFrame(self.utils.grab_section(report,
self.utils.grab_section( self.SCAN_META,
report, end=[self.CATEGORY_HEADER],
self.QID_HEADER, pop_last=True),
end=[self.GROUP_HEADER], columns=self.SCAN_META)
pop_last=True),
columns=self.QID_HEADER), dict_tracker['CATEGORY_HEADER'] = pd.DataFrame(self.utils.grab_section(report,
'GROUP_HEADER': pd.DataFrame( self.CATEGORY_HEADER),
self.utils.grab_section( columns=self.CATEGORY_HEADER)
report, all_dataframes.append(dict_tracker)
self.GROUP_HEADER,
end=[self.OWASP_HEADER], return all_dataframes
pop_last=True),
columns=self.GROUP_HEADER),
'OWASP_HEADER': pd.DataFrame(
self.utils.grab_section(
report,
self.OWASP_HEADER,
end=[self.WASC_HEADER],
pop_last=True),
columns=self.OWASP_HEADER),
'WASC_HEADER': pd.DataFrame(
self.utils.grab_section(
report,
self.WASC_HEADER,
end=[['APPENDIX']],
pop_last=True),
columns=self.WASC_HEADER),
'SCAN_META': pd.DataFrame(
self.utils.grab_section(report,
self.SCAN_META,
end=[self.CATEGORY_HEADER],
pop_last=True),
columns=self.SCAN_META),
'CATEGORY_HEADER': pd.DataFrame(
self.utils.grab_section(report,
self.CATEGORY_HEADER),
columns=self.CATEGORY_HEADER)
}
def data_normalizer(self, dataframes): def data_normalizer(self, dataframes):
""" """
@ -321,21 +409,12 @@ class qualysScanReport:
:param dataframes: :param dataframes:
:return: :return:
""" """
df_dict = dataframes df_dict = dataframes[0]
merged_df = pd.concat([ merged_df = pd.concat([df_dict['WEB_SCAN_VULN_BLOCK'], df_dict['WEB_SCAN_SENSITIVE_BLOCK'],
df_dict['WEB_SCAN_VULN_BLOCK'], df_dict['WEB_SCAN_INFO_BLOCK']], axis=0,
df_dict['WEB_SCAN_SENSITIVE_BLOCK'], ignore_index=False)
df_dict['WEB_SCAN_INFO_BLOCK'] merged_df = pd.merge(merged_df, df_dict['QID_HEADER'], left_on='QID',
], axis=0, ignore_index=False) right_on='Id')
merged_df = pd.merge(
merged_df,
df_dict['QID_HEADER'].drop(
#these columns always seem to be the same as what we're merging into
['CVSS V3 Attack Vector', 'CVSS V3 Base', 'CVSS V3 Temporal'],
axis=1),
left_on='QID', right_on='Id'
)
if 'Content' not in merged_df: if 'Content' not in merged_df:
merged_df['Content'] = '' merged_df['Content'] = ''
@ -352,11 +431,8 @@ class qualysScanReport:
merged_df = merged_df.assign(**df_dict['SCAN_META'].to_dict(orient='records')[0]) merged_df = merged_df.assign(**df_dict['SCAN_META'].to_dict(orient='records')[0])
merged_df = pd.merge( merged_df = pd.merge(merged_df, df_dict['CATEGORY_HEADER'], how='left', left_on=['Category', 'Severity Level'],
merged_df, df_dict['CATEGORY_HEADER'], right_on=['Category', 'Severity'], suffixes=('Severity', 'CatSev'))
how='left', left_on=['Category', 'Severity Level'],
right_on=['Category', 'Severity'], suffixes=('Severity', 'CatSev')
)
merged_df = merged_df.replace('N/A', '').fillna('') merged_df = merged_df.replace('N/A', '').fillna('')

View File

@ -1,18 +1,15 @@
from __future__ import absolute_import
import json import json
import os import os
from datetime import datetime, date from datetime import datetime, date, timedelta
from jira import JIRA from jira import JIRA
import requests
import logging import logging
from bottle import template from bottle import template
import re import re
from six.moves import range
class JiraAPI(object): class JiraAPI(object):
def __init__(self, hostname=None, username=None, password=None, path="", debug=False, clean_obsolete=True, def __init__(self, hostname=None, username=None, password=None, path="", debug=False, clean_obsolete=True, max_time_window=12, decommission_time_window=3):
max_time_window=12, decommission_time_window=3):
self.logger = logging.getLogger('JiraAPI') self.logger = logging.getLogger('JiraAPI')
if debug: if debug:
self.logger.setLevel(logging.DEBUG) self.logger.setLevel(logging.DEBUG)
@ -32,31 +29,26 @@ class JiraAPI(object):
self.template_path = 'vulnwhisp/reporting/resources/ticket.tpl' self.template_path = 'vulnwhisp/reporting/resources/ticket.tpl'
self.max_ips_ticket = 30 self.max_ips_ticket = 30
self.attachment_filename = "vulnerable_assets.txt" self.attachment_filename = "vulnerable_assets.txt"
self.max_time_tracking = max_time_window # in months self.max_time_tracking = max_time_window #in months
if path: if path:
self.download_tickets(path) self.download_tickets(path)
else: else:
self.logger.warn("No local path specified, skipping Jira ticket download.") self.logger.warn("No local path specified, skipping Jira ticket download.")
self.max_decommission_time = decommission_time_window # in months self.max_decommission_time = decommission_time_window #in months
# [HIGIENE] close tickets older than 12 months as obsolete (max_time_window defined) # [HIGIENE] close tickets older than 12 months as obsolete (max_time_window defined)
if clean_obsolete: if clean_obsolete:
self.close_obsolete_tickets() self.close_obsolete_tickets()
# deletes the tag "server_decommission" from those tickets closed <=3 months ago # deletes the tag "server_decommission" from those tickets closed <=3 months ago
self.decommission_cleanup() self.decommission_cleanup()
self.jira_still_vulnerable_comment = '''This ticket has been reopened due to the vulnerability not having been \ self.jira_still_vulnerable_comment = '''This ticket has been reopened due to the vulnerability not having been fixed (if multiple assets are affected, all need to be fixed; if the server is down, lastest known vulnerability might be the one reported).
fixed (if multiple assets are affected, all need to be fixed; if the server is down, lastest known \ - In the case of the team accepting the risk and wanting to close the ticket, please add the label "*risk_accepted*" to the ticket before closing it.
vulnerability might be the one reported). - If server has been decommissioned, please add the label "*server_decommission*" to the ticket before closing it.
- In the case of the team accepting the risk and wanting to close the ticket, please add the label \ - If when checking the vulnerability it looks like a false positive, _+please elaborate in a comment+_ and add the label "*false_positive*" before closing it; we will review it and report it to the vendor.
"*risk_accepted*" to the ticket before closing it.
- If server has been decommissioned, please add the label "*server_decommission*" to the ticket before closing \
it.
- If when checking the vulnerability it looks like a false positive, _+please elaborate in a comment+_ and add \
the label "*false_positive*" before closing it; we will review it and report it to the vendor.
If you have further doubts, please contact the Security Team.''' If you have further doubts, please contact the Security Team.'''
def create_ticket(self, title, desc, project="IS", components=[], tags=[], attachment_contents=[]): def create_ticket(self, title, desc, project="IS", components=[], tags=[], attachment_contents = []):
labels = ['vulnerability_management'] labels = ['vulnerability_management']
for tag in tags: for tag in tags:
labels.append(str(tag)) labels.append(str(tag))
@ -70,27 +62,32 @@ class JiraAPI(object):
for c in project_obj.components: for c in project_obj.components:
if component == c.name: if component == c.name:
self.logger.debug("resolved component name {} to id {}".format(c.name, c.id)) self.logger.debug("resolved component name {} to id {}".format(c.name, c.id))
components_ticket.append({"id": c.id}) components_ticket.append({ "id": c.id })
exists = True exists=True
if not exists: if not exists:
self.logger.error("Error creating Ticket: component {} not found".format(component)) self.logger.error("Error creating Ticket: component {} not found".format(component))
return 0 return 0
new_issue = self.jira.create_issue(project=project, try:
summary=title, new_issue = self.jira.create_issue(project=project,
description=desc, summary=title,
issuetype={'name': 'Bug'}, description=desc,
labels=labels, issuetype={'name': 'Bug'},
components=components_ticket) labels=labels,
components=components_ticket)
self.logger.info("Ticket {} created successfully".format(new_issue)) self.logger.info("Ticket {} created successfully".format(new_issue))
if attachment_contents: if attachment_contents:
self.add_content_as_attachment(new_issue, attachment_contents) self.add_content_as_attachment(new_issue, attachment_contents)
except Exception as e:
self.logger.error("Failed to create ticket on Jira Project '{}'. Error: {}".format(project, e))
new_issue = False
return new_issue return new_issue
# Basic JIRA Metrics #Basic JIRA Metrics
def metrics_open_tickets(self, project=None): def metrics_open_tickets(self, project=None):
jql = "labels= vulnerability_management and resolution = Unresolved" jql = "labels= vulnerability_management and resolution = Unresolved"
if project: if project:
@ -99,25 +96,22 @@ class JiraAPI(object):
return len(self.jira.search_issues(jql, maxResults=0)) return len(self.jira.search_issues(jql, maxResults=0))
def metrics_closed_tickets(self, project=None): def metrics_closed_tickets(self, project=None):
jql = "labels= vulnerability_management and NOT resolution = Unresolved AND created >=startOfMonth(-{})".format( jql = "labels= vulnerability_management and NOT resolution = Unresolved AND created >=startOfMonth(-{})".format(self.max_time_tracking)
self.max_time_tracking)
if project: if project:
jql += " and (project='{}')".format(project) jql += " and (project='{}')".format(project)
return len(self.jira.search_issues(jql, maxResults=0)) return len(self.jira.search_issues(jql, maxResults=0))
def sync(self, vulnerabilities, project, components=[]): def sync(self, vulnerabilities, project, components=[]):
# JIRA structure of each vulnerability: [source, scan_name, title, diagnosis, consequence, solution, #JIRA structure of each vulnerability: [source, scan_name, title, diagnosis, consequence, solution, ips, risk, references]
# ips, risk, references]
self.logger.info("JIRA Sync started") self.logger.info("JIRA Sync started")
for vuln in vulnerabilities: for vuln in vulnerabilities:
# JIRA doesn't allow labels with spaces, so making sure that the scan_name doesn't have spaces # JIRA doesn't allow labels with spaces, so making sure that the scan_name doesn't have spaces
# if it has, they will be replaced by "_" # if it has, they will be replaced by "_"
if " " in vuln['scan_name']: if " " in vuln['scan_name']:
vuln['scan_name'] = "_".join(vuln['scan_name'].split(" ")) vuln['scan_name'] = "_".join(vuln['scan_name'].split(" "))
# we exclude from the vulnerabilities to report those assets that already exist # we exclude from the vulnerabilities to report those assets that already exist with *risk_accepted*/*server_decommission*
# with *risk_accepted*/*server_decommission*
vuln = self.exclude_accepted_assets(vuln) vuln = self.exclude_accepted_assets(vuln)
# make sure after exclusion of risk_accepted assets there are still assets # make sure after exclusion of risk_accepted assets there are still assets
@ -142,17 +136,13 @@ class JiraAPI(object):
# create local text file with assets, attach it to ticket # create local text file with assets, attach it to ticket
if len(vuln['ips']) > self.max_ips_ticket: if len(vuln['ips']) > self.max_ips_ticket:
attachment_contents = vuln['ips'] attachment_contents = vuln['ips']
vuln['ips'] = [ vuln['ips'] = ["Affected hosts ({assets}) exceed Jira's allowed character limit, added as an attachment.".format(assets = len(attachment_contents))]
"Affected hosts ({assets}) exceed Jira's allowed character limit, added as an attachment.".format(
assets=len(attachment_contents))]
try: try:
tpl = template(self.template_path, vuln) tpl = template(self.template_path, vuln)
except Exception as e: except Exception as e:
self.logger.error('Exception templating: {}'.format(str(e))) self.logger.error('Exception templating: {}'.format(str(e)))
return 0 return 0
self.create_ticket(title=vuln['title'], desc=tpl, project=project, components=components, self.create_ticket(title=vuln['title'], desc=tpl, project=project, components=components, tags=[vuln['source'], vuln['scan_name'], 'vulnerability', vuln['risk']], attachment_contents = attachment_contents)
tags=[vuln['source'], vuln['scan_name'], 'vulnerability', vuln['risk']],
attachment_contents=attachment_contents)
else: else:
self.logger.info("Ignoring vulnerability as all assets are already reported in a risk_accepted ticket") self.logger.info("Ignoring vulnerability as all assets are already reported in a risk_accepted ticket")
@ -168,39 +158,34 @@ class JiraAPI(object):
labels = [vuln['source'], vuln['scan_name'], 'vulnerability_management', 'vulnerability'] labels = [vuln['source'], vuln['scan_name'], 'vulnerability_management', 'vulnerability']
if not self.excluded_tickets: if not self.excluded_tickets:
jql = "{} AND labels in (risk_accepted,server_decommission, false_positive) AND NOT labels=advisory AND created >=startOfMonth(-{})".format( jql = "{} AND labels in (risk_accepted,server_decommission, false_positive) AND NOT labels=advisory AND created >=startOfMonth(-{})".format(" AND ".join(["labels={}".format(label) for label in labels]), self.max_time_tracking)
" AND ".join(["labels={}".format(label) for label in labels]), self.max_time_tracking)
self.excluded_tickets = self.jira.search_issues(jql, maxResults=0) self.excluded_tickets = self.jira.search_issues(jql, maxResults=0)
title = vuln['title'] title = vuln['title']
# WARNING: function IGNORES DUPLICATES, after finding a "duplicate" will just return it exists #WARNING: function IGNORES DUPLICATES, after finding a "duplicate" will just return it exists
# it wont iterate over the rest of tickets looking for other possible duplicates/similar issues #it wont iterate over the rest of tickets looking for other possible duplicates/similar issues
self.logger.info("Comparing vulnerability to risk_accepted tickets") self.logger.info("Comparing vulnerability to risk_accepted tickets")
assets_to_exclude = [] assets_to_exclude = []
tickets_excluded_assets = [] tickets_excluded_assets = []
for index in range(len(self.excluded_tickets)): for index in range(len(self.excluded_tickets)):
checking_ticketid, checking_title, checking_assets = self.ticket_get_unique_fields( checking_ticketid, checking_title, checking_assets = self.ticket_get_unique_fields(self.excluded_tickets[index])
self.excluded_tickets[index])
if title.encode('ascii') == checking_title.encode('ascii'): if title.encode('ascii') == checking_title.encode('ascii'):
if checking_assets: if checking_assets:
# checking_assets is a list, we add to our full list for later delete all assets #checking_assets is a list, we add to our full list for later delete all assets
assets_to_exclude += checking_assets assets_to_exclude+=checking_assets
tickets_excluded_assets.append(checking_ticketid) tickets_excluded_assets.append(checking_ticketid)
if assets_to_exclude: if assets_to_exclude:
assets_to_remove = [] assets_to_remove = []
self.logger.warn("Vulnerable Assets seen on an already existing risk_accepted Jira ticket: {}".format( self.logger.warn("Vulnerable Assets seen on an already existing risk_accepted Jira ticket: {}".format(', '.join(tickets_excluded_assets)))
', '.join(tickets_excluded_assets)))
self.logger.debug("Original assets: {}".format(vuln['ips'])) self.logger.debug("Original assets: {}".format(vuln['ips']))
# assets in vulnerability have the structure "ip - hostname - port", so we need to match by partial #assets in vulnerability have the structure "ip - hostname - port", so we need to match by partial
for exclusion in assets_to_exclude: for exclusion in assets_to_exclude:
# for efficiency, we walk the backwards the array of ips from the scanners, as we will be popping out the matches # for efficiency, we walk the backwards the array of ips from the scanners, as we will be popping out the matches
# and we don't want it to affect the rest of the processing (otherwise, it would miss the asset right after the removed one) # and we don't want it to affect the rest of the processing (otherwise, it would miss the asset right after the removed one)
for index in range(len(vuln['ips']))[::-1]: for index in range(len(vuln['ips']))[::-1]:
if exclusion == vuln['ips'][index].split(" - ")[0]: if exclusion == vuln['ips'][index].split(" - ")[0]:
self.logger.debug( self.logger.debug("Deleting asset {} from vulnerability {}, seen in risk_accepted.".format(vuln['ips'][index], title))
"Deleting asset {} from vulnerability {}, seen in risk_accepted.".format(vuln['ips'][index],
title))
vuln['ips'].pop(index) vuln['ips'].pop(index)
self.logger.debug("Modified assets: {}".format(vuln['ips'])) self.logger.debug("Modified assets: {}".format(vuln['ips']))
@ -212,37 +197,35 @@ class JiraAPI(object):
Returns [exists (bool), is equal (bool), ticketid (str), assets (array)] Returns [exists (bool), is equal (bool), ticketid (str), assets (array)]
''' '''
# we need to return if the vulnerability has already been reported and the ID of the ticket for further processing # we need to return if the vulnerability has already been reported and the ID of the ticket for further processing
# function returns array [duplicated(bool), update(bool), ticketid, ticket_assets] #function returns array [duplicated(bool), update(bool), ticketid, ticket_assets]
title = vuln['title'] title = vuln['title']
labels = [vuln['source'], vuln['scan_name'], 'vulnerability_management', 'vulnerability'] labels = [vuln['source'], vuln['scan_name'], 'vulnerability_management', 'vulnerability']
# list(set()) to remove duplicates #list(set()) to remove duplicates
assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", ",".join(vuln['ips'])))) assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", ",".join(vuln['ips']))))
if not self.all_tickets: if not self.all_tickets:
self.logger.info("Retrieving all JIRA tickets with the following tags {}".format(labels)) self.logger.info("Retrieving all JIRA tickets with the following tags {}".format(labels))
# we want to check all JIRA tickets, to include tickets moved to other queues # we want to check all JIRA tickets, to include tickets moved to other queues
# will exclude tickets older than 12 months, old tickets will get closed for higiene and recreated if still vulnerable # will exclude tickets older than 12 months, old tickets will get closed for higiene and recreated if still vulnerable
jql = "{} AND NOT labels=advisory AND created >=startOfMonth(-{})".format( jql = "{} AND NOT labels=advisory AND created >=startOfMonth(-{})".format(" AND ".join(["labels={}".format(label) for label in labels]), self.max_time_tracking)
" AND ".join(["labels={}".format(label) for label in labels]), self.max_time_tracking)
self.all_tickets = self.jira.search_issues(jql, maxResults=0) self.all_tickets = self.jira.search_issues(jql, maxResults=0)
# WARNING: function IGNORES DUPLICATES, after finding a "duplicate" will just return it exists #WARNING: function IGNORES DUPLICATES, after finding a "duplicate" will just return it exists
# it wont iterate over the rest of tickets looking for other possible duplicates/similar issues #it wont iterate over the rest of tickets looking for other possible duplicates/similar issues
self.logger.info("Comparing Vulnerabilities to created tickets") self.logger.info("Comparing Vulnerabilities to created tickets")
for index in range(len(self.all_tickets)): for index in range(len(self.all_tickets)):
checking_ticketid, checking_title, checking_assets = self.ticket_get_unique_fields(self.all_tickets[index]) checking_ticketid, checking_title, checking_assets = self.ticket_get_unique_fields(self.all_tickets[index])
# added "not risk_accepted", as if it is risk_accepted, we will create a new ticket excluding the accepted assets # added "not risk_accepted", as if it is risk_accepted, we will create a new ticket excluding the accepted assets
if title.encode('ascii') == checking_title.encode('ascii') and not self.is_risk_accepted( if title.encode('ascii') == checking_title.encode('ascii') and not self.is_risk_accepted(self.jira.issue(checking_ticketid)):
self.jira.issue(checking_ticketid)):
difference = list(set(assets).symmetric_difference(checking_assets)) difference = list(set(assets).symmetric_difference(checking_assets))
# to check intersection - set(assets) & set(checking_assets) #to check intersection - set(assets) & set(checking_assets)
if difference: if difference:
self.logger.info("Asset mismatch, ticket to update. Ticket ID: {}".format(checking_ticketid)) self.logger.info("Asset mismatch, ticket to update. Ticket ID: {}".format(checking_ticketid))
return False, True, checking_ticketid, checking_assets # this will automatically validate return False, True, checking_ticketid, checking_assets #this will automatically validate
else: else:
self.logger.info("Confirmed duplicated. TickedID: {}".format(checking_ticketid)) self.logger.info("Confirmed duplicated. TickedID: {}".format(checking_ticketid))
return True, False, checking_ticketid, [] # this will automatically validate return True, False, checking_ticketid, [] #this will automatically validate
return False, False, "", [] return False, False, "", []
def ticket_get_unique_fields(self, ticket): def ticket_get_unique_fields(self, ticket):
@ -251,22 +234,19 @@ class JiraAPI(object):
assets = self.get_assets_from_description(ticket) assets = self.get_assets_from_description(ticket)
if not assets: if not assets:
# check if attachment, if so, get assets from attachment #check if attachment, if so, get assets from attachment
assets = self.get_assets_from_attachment(ticket) assets = self.get_assets_from_attachment(ticket)
return ticketid, title, assets return ticketid, title, assets
def get_assets_from_description(self, ticket, _raw=False): def get_assets_from_description(self, ticket, _raw = False):
# Get the assets as a string "host - protocol/port - hostname" separated by "\n" # Get the assets as a string "host - protocol/port - hostname" separated by "\n"
# structure the text to have the same structure as the assets from the attachment # structure the text to have the same structure as the assets from the attachment
affected_assets = "" affected_assets = ""
try: try:
affected_assets = \ affected_assets = ticket.raw.get('fields', {}).get('description').encode("ascii").split("{panel:title=Affected Assets}")[1].split("{panel}")[0].replace('\n','').replace(' * ','\n').replace('\n', '', 1)
ticket.raw.get('fields', {}).get('description').encode("ascii").split("{panel:title=Affected Assets}")[
1].split("{panel}")[0].replace('\n', '').replace(' * ', '\n').replace('\n', '', 1)
except Exception as e: except Exception as e:
self.logger.error( self.logger.error("Unable to process the Ticket's 'Affected Assets'. Ticket ID: {}. Reason: {}".format(ticket, e))
"Unable to process the Ticket's 'Affected Assets'. Ticket ID: {}. Reason: {}".format(ticket, e))
if affected_assets: if affected_assets:
if _raw: if _raw:
@ -282,14 +262,14 @@ class JiraAPI(object):
self.logger.error("Ticket IPs regex failed. Ticket ID: {}. Reason: {}".format(ticket, e)) self.logger.error("Ticket IPs regex failed. Ticket ID: {}. Reason: {}".format(ticket, e))
return False return False
def get_assets_from_attachment(self, ticket, _raw=False): def get_assets_from_attachment(self, ticket, _raw = False):
# Get the assets as a string "host - protocol/port - hostname" separated by "\n" # Get the assets as a string "host - protocol/port - hostname" separated by "\n"
affected_assets = [] affected_assets = []
try: try:
fields = self.jira.issue(ticket.key).raw.get('fields', {}) fields = self.jira.issue(ticket.key).raw.get('fields', {})
attachments = fields.get('attachment', {}) attachments = fields.get('attachment', {})
affected_assets = "" affected_assets = ""
# we will make sure we get the latest version of the file #we will make sure we get the latest version of the file
latest = '' latest = ''
attachment_id = '' attachment_id = ''
if attachments: if attachments:
@ -305,8 +285,7 @@ class JiraAPI(object):
affected_assets = self.jira.attachment(attachment_id).get() affected_assets = self.jira.attachment(attachment_id).get()
except Exception as e: except Exception as e:
self.logger.error( self.logger.error("Failed to get assets from ticket attachment. Ticket ID: {}. Reason: {}".format(ticket, e))
"Failed to get assets from ticket attachment. Ticket ID: {}. Reason: {}".format(ticket, e))
if affected_assets: if affected_assets:
if _raw: if _raw:
@ -352,15 +331,15 @@ class JiraAPI(object):
def add_content_as_attachment(self, issue, contents): def add_content_as_attachment(self, issue, contents):
try: try:
# Create the file locally with the data #Create the file locally with the data
attachment_file = open(self.attachment_filename, "w") attachment_file = open(self.attachment_filename, "w")
attachment_file.write("\n".join(contents)) attachment_file.write("\n".join(contents))
attachment_file.close() attachment_file.close()
# Push the created file to the ticket #Push the created file to the ticket
attachment_file = open(self.attachment_filename, "rb") attachment_file = open(self.attachment_filename, "rb")
self.jira.add_attachment(issue, attachment_file, self.attachment_filename) self.jira.add_attachment(issue, attachment_file, self.attachment_filename)
attachment_file.close() attachment_file.close()
# remove the temp file #remove the temp file
os.remove(self.attachment_filename) os.remove(self.attachment_filename)
self.logger.info("Added attachment successfully.") self.logger.info("Added attachment successfully.")
except: except:
@ -370,23 +349,21 @@ class JiraAPI(object):
return True return True
def get_ticket_reported_assets(self, ticket): def get_ticket_reported_assets(self, ticket):
# [METRICS] return a list with all the affected assets for that vulnerability (including already resolved ones) #[METRICS] return a list with all the affected assets for that vulnerability (including already resolved ones)
return list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", str(self.jira.issue(ticket).raw)))) return list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b",str(self.jira.issue(ticket).raw))))
def get_resolution_time(self, ticket): def get_resolution_time(self, ticket):
# get time a ticket took to be resolved #get time a ticket took to be resolved
ticket_obj = self.jira.issue(ticket) ticket_obj = self.jira.issue(ticket)
if self.is_ticket_resolved(ticket_obj): if self.is_ticket_resolved(ticket_obj):
ticket_data = ticket_obj.raw.get('fields') ticket_data = ticket_obj.raw.get('fields')
# dates follow format '2018-11-06T10:36:13.849+0100' #dates follow format '2018-11-06T10:36:13.849+0100'
created = [int(x) for x in created = [int(x) for x in ticket_data['created'].split('.')[0].replace('T', '-').replace(':','-').split('-')]
ticket_data['created'].split('.')[0].replace('T', '-').replace(':', '-').split('-')] resolved =[int(x) for x in ticket_data['resolutiondate'].split('.')[0].replace('T', '-').replace(':','-').split('-')]
resolved = [int(x) for x in
ticket_data['resolutiondate'].split('.')[0].replace('T', '-').replace(':', '-').split('-')]
start = datetime(created[0], created[1], created[2], created[3], created[4], created[5]) start = datetime(created[0],created[1],created[2],created[3],created[4],created[5])
end = datetime(resolved[0], resolved[1], resolved[2], resolved[3], resolved[4], resolved[5]) end = datetime(resolved[0],resolved[1],resolved[2],resolved[3],resolved[4],resolved[5])
return (end - start).days return (end-start).days
else: else:
self.logger.error("Ticket {ticket} is not resolved, can't calculate resolution time".format(ticket=ticket)) self.logger.error("Ticket {ticket} is not resolved, can't calculate resolution time".format(ticket=ticket))
@ -396,11 +373,11 @@ class JiraAPI(object):
# correct description will always be in the vulnerability to report, only needed to update description to new one # correct description will always be in the vulnerability to report, only needed to update description to new one
self.logger.info("Ticket {} exists, UPDATE requested".format(ticketid)) self.logger.info("Ticket {} exists, UPDATE requested".format(ticketid))
# for now, if a vulnerability has been accepted ('accepted_risk'), ticket is completely ignored and not updated (no new assets) #for now, if a vulnerability has been accepted ('accepted_risk'), ticket is completely ignored and not updated (no new assets)
# TODO when vulnerability accepted, create a new ticket with only the non-accepted vulnerable assets #TODO when vulnerability accepted, create a new ticket with only the non-accepted vulnerable assets
# this would require go through the downloaded tickets, check duplicates/accepted ones, and if so, #this would require go through the downloaded tickets, check duplicates/accepted ones, and if so,
# check on their assets to exclude them from the new ticket #check on their assets to exclude them from the new ticket
risk_accepted = False risk_accepted = False
ticket_obj = self.jira.issue(ticketid) ticket_obj = self.jira.issue(ticketid)
if self.is_ticket_resolved(ticket_obj): if self.is_ticket_resolved(ticket_obj):
@ -408,7 +385,7 @@ class JiraAPI(object):
return 0 return 0
self.reopen_ticket(ticketid=ticketid, comment=self.jira_still_vulnerable_comment) self.reopen_ticket(ticketid=ticketid, comment=self.jira_still_vulnerable_comment)
# First will do the comparison of assets #First will do the comparison of assets
ticket_obj.update() ticket_obj.update()
assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", ",".join(vuln['ips'])))) assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", ",".join(vuln['ips']))))
difference = list(set(assets).symmetric_difference(ticket_assets)) difference = list(set(assets).symmetric_difference(ticket_assets))
@ -416,7 +393,7 @@ class JiraAPI(object):
comment = '' comment = ''
added = '' added = ''
removed = '' removed = ''
# put a comment with the assets that have been added/removed #put a comment with the assets that have been added/removed
for asset in difference: for asset in difference:
if asset in assets: if asset in assets:
if not added: if not added:
@ -424,39 +401,36 @@ class JiraAPI(object):
added += '* {}\n'.format(asset) added += '* {}\n'.format(asset)
elif asset in ticket_assets: elif asset in ticket_assets:
if not removed: if not removed:
removed = '\nThe following assets *have been resolved*:\n' removed= '\nThe following assets *have been resolved*:\n'
removed += '* {}\n'.format(asset) removed += '* {}\n'.format(asset)
comment = added + removed comment = added + removed
# then will check if assets are too many that need to be added as an attachment #then will check if assets are too many that need to be added as an attachment
attachment_contents = [] attachment_contents = []
if len(vuln['ips']) > self.max_ips_ticket: if len(vuln['ips']) > self.max_ips_ticket:
attachment_contents = vuln['ips'] attachment_contents = vuln['ips']
vuln['ips'] = [ vuln['ips'] = ["Affected hosts ({assets}) exceed Jira's allowed character limit, added as an attachment.".format(assets = len(attachment_contents))]
"Affected hosts ({assets}) exceed Jira's allowed character limit, added as an attachment.".format(
assets=len(attachment_contents))]
# fill the ticket description template #fill the ticket description template
try: try:
tpl = template(self.template_path, vuln) tpl = template(self.template_path, vuln)
except Exception as e: except Exception as e:
self.logger.error('Exception updating assets: {}'.format(str(e))) self.logger.error('Exception updating assets: {}'.format(str(e)))
return 0 return 0
# proceed checking if it requires adding as an attachment #proceed checking if it requires adding as an attachment
try: try:
# update attachment with hosts and delete the old versions #update attachment with hosts and delete the old versions
if attachment_contents: if attachment_contents:
self.clean_old_attachments(ticket_obj) self.clean_old_attachments(ticket_obj)
self.add_content_as_attachment(ticket_obj, attachment_contents) self.add_content_as_attachment(ticket_obj, attachment_contents)
ticket_obj.update(description=tpl, comment=comment, fields={"labels": ticket_obj.fields.labels}) ticket_obj.update(description=tpl, comment=comment, fields={"labels":ticket_obj.fields.labels})
self.logger.info("Ticket {} updated successfully".format(ticketid)) self.logger.info("Ticket {} updated successfully".format(ticketid))
self.add_label(ticketid, 'updated') self.add_label(ticketid, 'updated')
except Exception as e: except Exception as e:
self.logger.error( self.logger.error("Error while trying up update ticket {ticketid}.\nReason: {e}".format(ticketid = ticketid, e=e))
"Error while trying up update ticket {ticketid}.\nReason: {e}".format(ticketid=ticketid, e=e))
return 0 return 0
def add_label(self, ticketid, label): def add_label(self, ticketid, label):
@ -466,11 +440,10 @@ class JiraAPI(object):
ticket_obj.fields.labels.append(label) ticket_obj.fields.labels.append(label)
try: try:
ticket_obj.update(fields={"labels": ticket_obj.fields.labels}) ticket_obj.update(fields={"labels":ticket_obj.fields.labels})
self.logger.info("Added label {label} to ticket {ticket}".format(label=label, ticket=ticketid)) self.logger.info("Added label {label} to ticket {ticket}".format(label=label, ticket=ticketid))
except Exception as e: except:
self.logger.error( self.logger.error("Error while trying to add label {label} to ticket {ticket}".format(label=label, ticket=ticketid))
"Error while trying to add label {label} to ticket {ticket}".format(label=label, ticket=ticketid))
return 0 return 0
@ -481,11 +454,10 @@ class JiraAPI(object):
ticket_obj.fields.labels.remove(label) ticket_obj.fields.labels.remove(label)
try: try:
ticket_obj.update(fields={"labels": ticket_obj.fields.labels}) ticket_obj.update(fields={"labels":ticket_obj.fields.labels})
self.logger.info("Removed label {label} from ticket {ticket}".format(label=label, ticket=ticketid)) self.logger.info("Removed label {label} from ticket {ticket}".format(label=label, ticket=ticketid))
except Exception as e: except:
self.logger.error("Error while trying to remove label {label} to ticket {ticket}".format(label=label, self.logger.error("Error while trying to remove label {label} to ticket {ticket}".format(label=label, ticket=ticketid))
ticket=ticketid))
else: else:
self.logger.error("Error: label {label} not in ticket {ticket}".format(label=label, ticket=ticketid)) self.logger.error("Error: label {label} not in ticket {ticket}".format(label=label, ticket=ticketid))
@ -511,13 +483,14 @@ class JiraAPI(object):
self.close_ticket(ticket, self.JIRA_RESOLUTION_FIXED, comment) self.close_ticket(ticket, self.JIRA_RESOLUTION_FIXED, comment)
return 0 return 0
def is_ticket_reopenable(self, ticket_obj): def is_ticket_reopenable(self, ticket_obj):
transitions = self.jira.transitions(ticket_obj) transitions = self.jira.transitions(ticket_obj)
for transition in transitions: for transition in transitions:
if transition.get('name') == self.JIRA_REOPEN_ISSUE: if transition.get('name') == self.JIRA_REOPEN_ISSUE:
self.logger.debug("Ticket is reopenable") self.logger.debug("Ticket is reopenable")
return True return True
self.logger.warn("Ticket can't be opened. Check Jira transitions.") self.logger.error("Ticket {} can't be opened. Check Jira transitions.".format(ticket_obj))
return False return False
def is_ticket_closeable(self, ticket_obj): def is_ticket_closeable(self, ticket_obj):
@ -525,11 +498,11 @@ class JiraAPI(object):
for transition in transitions: for transition in transitions:
if transition.get('name') == self.JIRA_CLOSE_ISSUE: if transition.get('name') == self.JIRA_CLOSE_ISSUE:
return True return True
self.logger.warn("Ticket can't closed. Check Jira transitions.") self.logger.error("Ticket {} can't closed. Check Jira transitions.".format(ticket_obj))
return False return False
def is_ticket_resolved(self, ticket_obj): def is_ticket_resolved(self, ticket_obj):
# Checks if a ticket is resolved or not #Checks if a ticket is resolved or not
if ticket_obj is not None: if ticket_obj is not None:
if ticket_obj.raw['fields'].get('resolution') is not None: if ticket_obj.raw['fields'].get('resolution') is not None:
if ticket_obj.raw['fields'].get('resolution').get('name') != 'Unresolved': if ticket_obj.raw['fields'].get('resolution').get('name') != 'Unresolved':
@ -539,6 +512,7 @@ class JiraAPI(object):
self.logger.debug("Checked ticket {} is already open".format(ticket_obj)) self.logger.debug("Checked ticket {} is already open".format(ticket_obj))
return False return False
def is_risk_accepted(self, ticket_obj): def is_risk_accepted(self, ticket_obj):
if ticket_obj is not None: if ticket_obj is not None:
if ticket_obj.raw['fields'].get('labels') is not None: if ticket_obj.raw['fields'].get('labels') is not None:
@ -564,8 +538,7 @@ class JiraAPI(object):
if (not self.is_risk_accepted(ticket_obj) or ignore_labels): if (not self.is_risk_accepted(ticket_obj) or ignore_labels):
try: try:
if self.is_ticket_reopenable(ticket_obj): if self.is_ticket_reopenable(ticket_obj):
error = self.jira.transition_issue(issue=ticketid, transition=self.JIRA_REOPEN_ISSUE, error = self.jira.transition_issue(issue=ticketid, transition=self.JIRA_REOPEN_ISSUE, comment = comment)
comment=comment)
self.logger.info("Ticket {} reopened successfully".format(ticketid)) self.logger.info("Ticket {} reopened successfully".format(ticketid))
if not ignore_labels: if not ignore_labels:
self.add_label(ticketid, 'reopened') self.add_label(ticketid, 'reopened')
@ -583,10 +556,9 @@ class JiraAPI(object):
if not self.is_ticket_resolved(ticket_obj): if not self.is_ticket_resolved(ticket_obj):
try: try:
if self.is_ticket_closeable(ticket_obj): if self.is_ticket_closeable(ticket_obj):
# need to add the label before closing the ticket #need to add the label before closing the ticket
self.add_label(ticketid, 'closed') self.add_label(ticketid, 'closed')
error = self.jira.transition_issue(issue=ticketid, transition=self.JIRA_CLOSE_ISSUE, error = self.jira.transition_issue(issue=ticketid, transition=self.JIRA_CLOSE_ISSUE, comment = comment, resolution = {"name": resolution })
comment=comment, resolution={"name": resolution})
self.logger.info("Ticket {} closed successfully".format(ticketid)) self.logger.info("Ticket {} closed successfully".format(ticketid))
return 1 return 1
except Exception as e: except Exception as e:
@ -599,15 +571,14 @@ class JiraAPI(object):
def close_obsolete_tickets(self): def close_obsolete_tickets(self):
# Close tickets older than 12 months, vulnerabilities not solved will get created a new ticket # Close tickets older than 12 months, vulnerabilities not solved will get created a new ticket
self.logger.info("Closing obsolete tickets older than {} months".format(self.max_time_tracking)) self.logger.info("Closing obsolete tickets older than {} months".format(self.max_time_tracking))
jql = "labels=vulnerability_management AND NOT labels=advisory AND created <startOfMonth(-{}) and resolution=Unresolved".format( jql = "labels=vulnerability_management AND NOT labels=advisory AND created <startOfMonth(-{}) and resolution=Unresolved".format(self.max_time_tracking)
self.max_time_tracking)
tickets_to_close = self.jira.search_issues(jql, maxResults=0) tickets_to_close = self.jira.search_issues(jql, maxResults=0)
comment = '''This ticket is being closed for hygiene, as it is more than {} months old. comment = '''This ticket is being closed for hygiene, as it is more than {} months old.
If the vulnerability still exists, a new ticket will be opened.'''.format(self.max_time_tracking) If the vulnerability still exists, a new ticket will be opened.'''.format(self.max_time_tracking)
for ticket in tickets_to_close: for ticket in tickets_to_close:
self.close_ticket(ticket, self.JIRA_RESOLUTION_OBSOLETE, comment) self.close_ticket(ticket, self.JIRA_RESOLUTION_OBSOLETE, comment)
return 0 return 0
@ -623,7 +594,7 @@ class JiraAPI(object):
''' '''
saves all tickets locally, local snapshot of vulnerability_management ticktes saves all tickets locally, local snapshot of vulnerability_management ticktes
''' '''
# check if file already exists #check if file already exists
check_date = str(date.today()) check_date = str(date.today())
fname = '{}jira_{}.json'.format(path, check_date) fname = '{}jira_{}.json'.format(path, check_date)
if os.path.isfile(fname): if os.path.isfile(fname):
@ -631,11 +602,10 @@ class JiraAPI(object):
return True return True
try: try:
self.logger.info("Saving locally tickets from the last {} months".format(self.max_time_tracking)) self.logger.info("Saving locally tickets from the last {} months".format(self.max_time_tracking))
jql = "labels=vulnerability_management AND NOT labels=advisory AND created >=startOfMonth(-{})".format( jql = "labels=vulnerability_management AND NOT labels=advisory AND created >=startOfMonth(-{})".format(self.max_time_tracking)
self.max_time_tracking)
tickets_data = self.jira.search_issues(jql, maxResults=0) tickets_data = self.jira.search_issues(jql, maxResults=0)
# TODO process tickets, creating a new field called "_metadata" with all the affected assets well structured #TODO process tickets, creating a new field called "_metadata" with all the affected assets well structured
# for future processing in ELK/Splunk; this includes downloading attachments with assets and processing them # for future processing in ELK/Splunk; this includes downloading attachments with assets and processing them
processed_tickets = [] processed_tickets = []
@ -656,13 +626,14 @@ class JiraAPI(object):
assets_json = self.parse_asset_to_json(assets) assets_json = self.parse_asset_to_json(assets)
_metadata["affected_hosts"].append(assets_json) _metadata["affected_hosts"].append(assets_json)
temp_ticket = ticket.raw.get('fields') temp_ticket = ticket.raw.get('fields')
temp_ticket['_metadata'] = _metadata temp_ticket['_metadata'] = _metadata
processed_tickets.append(temp_ticket) processed_tickets.append(temp_ticket)
# end of line needed, as writelines() doesn't add it automatically, otherwise one big line #end of line needed, as writelines() doesn't add it automatically, otherwise one big line
to_save = [json.dumps(ticket.raw.get('fields')) + "\n" for ticket in tickets_data] to_save = [json.dumps(ticket.raw.get('fields'))+"\n" for ticket in tickets_data]
with open(fname, 'w') as outfile: with open(fname, 'w') as outfile:
outfile.writelines(to_save) outfile.writelines(to_save)
self.logger.info("Tickets saved succesfully.") self.logger.info("Tickets saved succesfully.")
@ -680,20 +651,17 @@ class JiraAPI(object):
closed already for more than x months (default is 3 months) in order to clean solved issues closed already for more than x months (default is 3 months) in order to clean solved issues
for statistics purposes for statistics purposes
''' '''
self.logger.info("Deleting 'server_decommission' tag from tickets closed more than {} months ago".format( self.logger.info("Deleting 'server_decommission' tag from tickets closed more than {} months ago".format(self.max_decommission_time))
self.max_decommission_time))
jql = "labels=vulnerability_management AND labels=server_decommission and resolutiondate <=startOfMonth(-{})".format( jql = "labels=vulnerability_management AND labels=server_decommission and resolutiondate <=startOfMonth(-{})".format(self.max_decommission_time)
self.max_decommission_time)
decommissioned_tickets = self.jira.search_issues(jql, maxResults=0) decommissioned_tickets = self.jira.search_issues(jql, maxResults=0)
comment = '''This ticket is having deleted the *server_decommission* tag, as it is more than {} months old and is expected to already have been decommissioned. comment = '''This ticket is having deleted the *server_decommission* tag, as it is more than {} months old and is expected to already have been decommissioned.
If that is not the case and the vulnerability still exists, the vulnerability will be opened again.'''.format( If that is not the case and the vulnerability still exists, the vulnerability will be opened again.'''.format(self.max_decommission_time)
self.max_decommission_time)
for ticket in decommissioned_tickets: for ticket in decommissioned_tickets:
# we open first the ticket, as we want to make sure the process is not blocked due to #we open first the ticket, as we want to make sure the process is not blocked due to
# an unexisting jira workflow or unallowed edit from closed tickets #an unexisting jira workflow or unallowed edit from closed tickets
self.reopen_ticket(ticketid=ticket, ignore_labels=True) self.reopen_ticket(ticketid=ticket, ignore_labels=True)
self.remove_label(ticket, 'server_decommission') self.remove_label(ticket, 'server_decommission')
self.close_ticket(ticket, self.JIRA_RESOLUTION_FIXED, comment) self.close_ticket(ticket, self.JIRA_RESOLUTION_FIXED, comment)

View File

@ -1,4 +1,3 @@
from __future__ import absolute_import
import os import os
import logging import logging
import httpretty import httpretty
@ -21,12 +20,10 @@ class mockAPI(object):
def get_directories(self, path): def get_directories(self, path):
dir, subdirs, files = next(os.walk(path)) dir, subdirs, files = next(os.walk(path))
self.logger.debug('Subdirectories found: {}'.format(subdirs))
return subdirs return subdirs
def get_files(self, path): def get_files(self, path):
dir, subdirs, files = next(os.walk(path)) dir, subdirs, files = next(os.walk(path))
self.logger.debug('Files found: {}'.format(files))
return files return files
def qualys_vuln_callback(self, request, uri, response_headers): def qualys_vuln_callback(self, request, uri, response_headers):

View File

@ -1,17 +1,13 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import
from six.moves import range
from functools import reduce
__author__ = 'Austin Taylor' __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_web import qualysScanReport from frameworks.qualys_web import qualysScanReport
from .frameworks.qualys_vuln import qualysVulnScan from frameworks.qualys_vuln import qualysVulnScan
from .frameworks.openvas import OpenVAS_API from frameworks.openvas import OpenVAS_API
from .reporting.jira_api import JiraAPI from reporting.jira_api import JiraAPI
import pandas as pd import pandas as pd
from lxml import objectify from lxml import objectify
import sys import sys
@ -25,6 +21,7 @@ import socket
class vulnWhispererBase(object): class vulnWhispererBase(object):
CONFIG_SECTION = None CONFIG_SECTION = None
def __init__( def __init__(
@ -38,13 +35,13 @@ class vulnWhispererBase(object):
password=None, password=None,
section=None, section=None,
develop=False, develop=False,
): ):
self.logger = logging.getLogger('vulnWhispererBase') self.logger = logging.getLogger('vulnWhispererBase')
if debug: if debug:
self.logger.setLevel(logging.DEBUG) self.logger.setLevel(logging.DEBUG)
if self.CONFIG_SECTION is None: if self.CONFIG_SECTION is None:
raise Exception('Implementing class must define CONFIG_SECTION') raise Exception('Implementing class must define CONFIG_SECTION')
self.exit_code = 0 self.exit_code = 0
self.db_name = db_name self.db_name = db_name
@ -68,6 +65,8 @@ class vulnWhispererBase(object):
self.db_path = self.config.get(self.CONFIG_SECTION, 'db_path') self.db_path = self.config.get(self.CONFIG_SECTION, 'db_path')
self.verbose = self.config.getbool(self.CONFIG_SECTION, 'verbose') self.verbose = self.config.getbool(self.CONFIG_SECTION, 'verbose')
if self.db_name is not None: if self.db_name is not None:
if self.db_path: if self.db_path:
self.database = os.path.join(self.db_path, self.database = os.path.join(self.db_path,
@ -89,8 +88,7 @@ class vulnWhispererBase(object):
self.cur = self.conn.cursor() self.cur = self.conn.cursor()
self.logger.info('Connected to database at {loc}'.format(loc=self.database)) self.logger.info('Connected to database at {loc}'.format(loc=self.database))
except Exception as e: except Exception as e:
self.logger.error( self.logger.error('Could not connect to database at {loc}\nReason: {e} - Please ensure the path exist'.format(
'Could not connect to database at {loc}\nReason: {e} - Please ensure the path exist'.format(
e=e, e=e,
loc=self.database)) loc=self.database))
else: else:
@ -123,7 +121,7 @@ class vulnWhispererBase(object):
' scan_name TEXT, scan_id INTEGER, last_modified DATE, filename TEXT,' ' scan_name TEXT, scan_id INTEGER, last_modified DATE, filename TEXT,'
' download_time DATE, record_count INTEGER, source TEXT,' ' download_time DATE, record_count INTEGER, source TEXT,'
' uuid TEXT, processed INTEGER, reported INTEGER)' ' uuid TEXT, processed INTEGER, reported INTEGER)'
) )
self.conn.commit() self.conn.commit()
def delete_table(self): def delete_table(self):
@ -149,11 +147,11 @@ class vulnWhispererBase(object):
return data return data
def record_insert(self, record): def record_insert(self, record):
# for backwards compatibility with older versions without "reported" field #for backwards compatibility with older versions without "reported" field
try: try:
# -1 to get the latest column, 1 to get the column name (old version would be "processed", new "reported") #-1 to get the latest column, 1 to get the column name (old version would be "processed", new "reported")
# TODO delete backward compatibility check after some versions #TODO delete backward compatibility check after some versions
last_column_table = self.cur.execute('PRAGMA table_info(scan_history)').fetchall()[-1][1] last_column_table = self.cur.execute('PRAGMA table_info(scan_history)').fetchall()[-1][1]
if last_column_table == self.table_columns[-1]: if last_column_table == self.table_columns[-1]:
self.cur.execute('insert into scan_history({table_columns}) values (?,?,?,?,?,?,?,?,?,?)'.format( self.cur.execute('insert into scan_history({table_columns}) values (?,?,?,?,?,?,?,?,?,?)'.format(
@ -168,8 +166,8 @@ class vulnWhispererBase(object):
sys.exit(1) sys.exit(1)
def set_latest_scan_reported(self, filename): def set_latest_scan_reported(self, filename):
# the reason to use the filename instead of the source/scan_name is because the filename already belongs to #the reason to use the filename instead of the source/scan_name is because the filename already belongs to
# that latest scan, and we maintain integrity making sure that it is the exact scan we checked #that latest scan, and we maintain integrity making sure that it is the exact scan we checked
try: try:
self.cur.execute('UPDATE scan_history SET reported = 1 WHERE filename="{}";'.format(filename)) self.cur.execute('UPDATE scan_history SET reported = 1 WHERE filename="{}";'.format(filename))
self.conn.commit() self.conn.commit()
@ -187,8 +185,7 @@ class vulnWhispererBase(object):
""" """
try: try:
self.conn.text_factory = str self.conn.text_factory = str
self.cur.execute('SELECT uuid FROM scan_history where source = "{config_section}"'.format( self.cur.execute('SELECT uuid FROM scan_history where source = "{config_section}"'.format(config_section=self.CONFIG_SECTION))
config_section=self.CONFIG_SECTION))
results = frozenset([r[0] for r in self.cur.fetchall()]) results = frozenset([r[0] for r in self.cur.fetchall()])
except: except:
results = [] results = []
@ -211,23 +208,18 @@ class vulnWhispererBase(object):
try: try:
self.conn.text_factory = str self.conn.text_factory = str
self.cur.execute( self.cur.execute('SELECT filename FROM scan_history WHERE source="{}" AND scan_name="{}" ORDER BY last_modified DESC LIMIT 1;'.format(source, scan_name))
'SELECT filename FROM scan_history WHERE source="{}" AND scan_name="{}" ORDER BY last_modified DESC LIMIT 1;'.format( #should always return just one filename
source, scan_name))
# should always return just one filename
results = [r[0] for r in self.cur.fetchall()][0] results = [r[0] for r in self.cur.fetchall()][0]
# -1 to get the latest column, 1 to get the column name (old version would be "processed", new "reported") #-1 to get the latest column, 1 to get the column name (old version would be "processed", new "reported")
# TODO delete backward compatibility check after some versions #TODO delete backward compatibility check after some versions
last_column_table = self.cur.execute('PRAGMA table_info(scan_history)').fetchall()[-1][1] last_column_table = self.cur.execute('PRAGMA table_info(scan_history)').fetchall()[-1][1]
if results and last_column_table == self.table_columns[-1]: if results and last_column_table == self.table_columns[-1]:
reported = self.cur.execute( reported = self.cur.execute('SELECT reported FROM scan_history WHERE filename="{}"'.format(results)).fetchall()
'SELECT reported FROM scan_history WHERE filename="{}"'.format(results)).fetchall()
reported = reported[0][0] reported = reported[0][0]
if reported: if reported:
self.logger.debug( self.logger.debug("Last downloaded scan from source {source} scan_name {scan_name} has already been reported".format(source=source, scan_name=scan_name))
"Last downloaded scan from source {source} scan_name {scan_name} has already been reported".format(
source=source, scan_name=scan_name))
except Exception as e: except Exception as e:
self.logger.error("Error when getting latest results from {}.{} : {}".format(source, scan_name, e)) self.logger.error("Error when getting latest results from {}.{} : {}".format(source, scan_name, e))
@ -256,14 +248,14 @@ class vulnWhispererBase(object):
self.cur.execute("SELECT DISTINCT scan_name FROM scan_history WHERE source='{}';".format(source)) self.cur.execute("SELECT DISTINCT scan_name FROM scan_history WHERE source='{}';".format(source))
scan_names = [r[0] for r in self.cur.fetchall()] scan_names = [r[0] for r in self.cur.fetchall()]
for scan in scan_names: for scan in scan_names:
results.append('{}.{}'.format(source, scan)) results.append('{}.{}'.format(source,scan))
except: except:
scan_names = [] scan_names = []
return results return results
class vulnWhispererNessus(vulnWhispererBase): class vulnWhispererNessus(vulnWhispererBase):
CONFIG_SECTION = None CONFIG_SECTION = None
def __init__( def __init__(
@ -277,7 +269,7 @@ class vulnWhispererNessus(vulnWhispererBase):
password=None, password=None,
profile='nessus' profile='nessus'
): ):
self.CONFIG_SECTION = profile self.CONFIG_SECTION=profile
super(vulnWhispererNessus, self).__init__(config=config) super(vulnWhispererNessus, self).__init__(config=config)
@ -299,8 +291,8 @@ class vulnWhispererNessus(vulnWhispererBase):
'trash') 'trash')
try: try:
self.access_key = self.config.get(self.CONFIG_SECTION, 'access_key') self.access_key = self.config.get(self.CONFIG_SECTION,'access_key')
self.secret_key = self.config.get(self.CONFIG_SECTION, 'secret_key') self.secret_key = self.config.get(self.CONFIG_SECTION,'secret_key')
except: except:
pass pass
@ -317,7 +309,7 @@ class vulnWhispererNessus(vulnWhispererBase):
) )
self.nessus_connect = True self.nessus_connect = True
self.logger.info('Connected to {} on {host}:{port}'.format(self.CONFIG_SECTION, host=self.hostname, self.logger.info('Connected to {} on {host}:{port}'.format(self.CONFIG_SECTION, host=self.hostname,
port=str(self.nessus_port))) port=str(self.nessus_port)))
except Exception as e: except Exception as e:
self.logger.error('Exception: {}'.format(str(e))) self.logger.error('Exception: {}'.format(str(e)))
raise Exception( raise Exception(
@ -328,7 +320,9 @@ class vulnWhispererNessus(vulnWhispererBase):
except Exception as e: except Exception as e:
self.logger.error('Could not properly load your config!\nReason: {e}'.format(e=e)) self.logger.error('Could not properly load your config!\nReason: {e}'.format(e=e))
return False return False
# sys.exit(1) #sys.exit(1)
def scan_count(self, scans, completed=False): def scan_count(self, scans, completed=False):
""" """
@ -371,6 +365,7 @@ class vulnWhispererNessus(vulnWhispererBase):
scan_records = [s for s in scan_records if s['status'] == 'completed'] scan_records = [s for s in scan_records if s['status'] == 'completed']
return scan_records return scan_records
def whisper_nessus(self): def whisper_nessus(self):
if self.nessus_connect: if self.nessus_connect:
scan_data = self.nessus.scans scan_data = self.nessus.scans
@ -425,8 +420,7 @@ class vulnWhispererNessus(vulnWhispererBase):
s['uuid'], s['uuid'],
) )
# TODO Create directory sync function which scans the directory for files that exist already and # TODO Create directory sync function which scans the directory for files that exist already and populates the database
# populates the database
folder_id = s['folder_id'] folder_id = s['folder_id']
if self.CONFIG_SECTION == 'tenable': if self.CONFIG_SECTION == 'tenable':
@ -456,26 +450,22 @@ class vulnWhispererNessus(vulnWhispererBase):
0, 0,
) )
self.record_insert(record_meta) self.record_insert(record_meta)
self.logger.info( self.logger.info('File {filename} already exist! Updating database'.format(filename=relative_path_name))
'File {filename} already exist! Updating database'.format(filename=relative_path_name))
else: else:
try: try:
file_req = \ file_req = \
self.nessus.download_scan(scan_id=scan_id, history=history_id, self.nessus.download_scan(scan_id=scan_id, history=history_id,
export_format='csv') export_format='csv')
except Exception as e: except Exception as e:
self.logger.error( self.logger.error('Could not download {} scan {}: {}'.format(self.CONFIG_SECTION, scan_id, str(e)))
'Could not download {} scan {}: {}'.format(self.CONFIG_SECTION, scan_id, str(e)))
self.exit_code += 1 self.exit_code += 1
continue continue
clean_csv = \ clean_csv = \
pd.read_csv(io.StringIO(file_req.decode('utf-8'))) pd.read_csv(io.StringIO(file_req.decode('utf-8')))
if len(clean_csv) > 2: if len(clean_csv) > 2:
self.logger.info('Processing {}/{} for scan: {}'.format(scan_count, len(scan_list), self.logger.info('Processing {}/{} for scan: {}'.format(scan_count, len(scan_list), scan_name.encode('utf8')))
scan_name.encode('utf8'))) columns_to_cleanse = ['CVSS','CVE','Description','Synopsis','Solution','See Also','Plugin Output', 'MAC Address']
columns_to_cleanse = ['CVSS', 'CVE', 'Description', 'Synopsis', 'Solution', 'See Also',
'Plugin Output', 'MAC Address']
for col in columns_to_cleanse: for col in columns_to_cleanse:
if col in clean_csv: if col in clean_csv:
@ -496,8 +486,7 @@ class vulnWhispererNessus(vulnWhispererBase):
) )
self.record_insert(record_meta) self.record_insert(record_meta)
self.logger.info('{filename} records written to {path} '.format(filename=clean_csv.shape[0], self.logger.info('{filename} records written to {path} '.format(filename=clean_csv.shape[0],
path=file_name.encode( path=file_name.encode('utf8')))
'utf8')))
else: else:
record_meta = ( record_meta = (
scan_name, scan_name,
@ -512,79 +501,66 @@ class vulnWhispererNessus(vulnWhispererBase):
0, 0,
) )
self.record_insert(record_meta) self.record_insert(record_meta)
self.logger.warn( self.logger.warn('{} has no host available... Updating database and skipping!'.format(file_name))
'{} has no host available... Updating database and skipping!'.format(file_name))
self.conn.close() self.conn.close()
self.logger.info('Scan aggregation complete! Connection to database closed.') self.logger.info('Scan aggregation complete! Connection to database closed.')
else: else:
self.logger.error( self.logger.error('Failed to use scanner at {host}:{port}'.format(host=self.hostname, port=self.nessus_port))
'Failed to use scanner at {host}:{port}'.format(host=self.hostname, port=self.nessus_port))
self.exit_code += 1 self.exit_code += 1
return self.exit_code return self.exit_code
class vulnWhispererQualys(vulnWhispererBase): class vulnWhispererQualys(vulnWhispererBase):
CONFIG_SECTION = 'qualys_web' CONFIG_SECTION = 'qualys_web'
COLUMN_MAPPING = {'Access Path': 'access_path', COLUMN_MAPPING = {'Access Path': 'access_path',
'Ajax Request': 'ajax_request', 'Ajax Request': 'ajax_request',
'Ajax Request ID': 'ajax_request_id', 'Ajax Request ID': 'ajax_request_id',
'Authentication': 'authentication', 'Authentication': 'authentication',
'CVSS Base': 'cvss', 'CVSS Base': 'cvss',
'CVSS V3 Attack Vector': 'cvss_v3_attack_vector', 'CVSS Temporal': 'cvss_temporal',
'CVSS V3 Base': 'cvss_v3_base', 'CWE': 'cwe',
'CVSS V3 Temporal': 'cvss_v3_temporal', 'Category': 'category',
'CVSS Temporal': 'cvss_temporal', 'Content': 'content',
'CWE': 'cwe', 'DescriptionSeverity': 'severity_description',
'Category': 'category', 'DescriptionCatSev': 'category_description',
'Content': 'content', 'Detection ID': 'detection_id',
'Custom Attributes': 'custom_attributes', 'Evidence #1': 'evidence_1',
'DescriptionSeverity': 'severity_description', 'First Time Detected': 'first_time_detected',
'DescriptionCatSev': 'category_description', 'Form Entry Point': 'form_entry_point',
'Detection ID': 'detection_id', 'Function': 'function',
'Evidence #1': 'evidence_1', 'Groups': 'groups',
'First Time Detected': 'first_time_detected', 'ID': 'id',
'Form Entry Point': 'form_entry_point', 'Ignore Comments': 'ignore_comments',
'Function': 'function', 'Ignore Date': 'ignore_date',
'Groups': 'groups', 'Ignore Reason': 'ignore_reason',
'ID': 'id', 'Ignore User': 'ignore_user',
'Ignore Comments': 'ignore_comments', 'Ignored': 'ignored',
'Ignore Date': 'ignore_date', 'Impact': 'impact',
'Ignore Reason': 'ignore_reason', 'Last Time Detected': 'last_time_detected',
'Ignore User': 'ignore_user', 'Last Time Tested': 'last_time_tested',
'Ignored': 'ignored', 'Level': 'level',
'Impact': 'impact', 'OWASP': 'owasp',
'Info#1': 'info_1', 'Operating System': 'operating_system',
'Last Time Detected': 'last_time_detected', 'Owner': 'owner',
'Last Time Tested': 'last_time_tested', 'Param': 'param',
'Level': 'level', 'Payload #1': 'payload_1',
'OWASP': 'owasp', 'QID': 'plugin_id',
'Operating System': 'operating_system', 'Request Headers #1': 'request_headers_1',
'Owner': 'owner', 'Request Method #1': 'request_method_1',
'Param/Cookie': 'param', 'Request URL #1': 'request_url_1',
'Payload #1': 'payload_1', 'Response #1': 'response_1',
'Port': 'port', 'Scope': 'scope',
'Protocol': 'protocol', 'Severity': 'risk',
'QID': 'plugin_id', 'Severity Level': 'security_level',
'Request Body #1': 'request_body_1', 'Solution': 'solution',
'Request Headers #1': 'request_headers_1', 'Times Detected': 'times_detected',
'Request Method #1': 'request_method_1', 'Title': 'plugin_name',
'Request URL #1': 'request_url_1', 'URL': 'url',
'Response #1': 'response_1', 'Url': 'uri',
'Scope': 'scope', 'Vulnerability Category': 'vulnerability_category',
'Severity': 'risk', 'WASC': 'wasc',
'Severity Level': 'security_level', 'Web Application Name': 'web_application_name'}
'Solution': 'solution',
'Tags': 'tags',
'Times Detected': 'times_detected',
'Title': 'plugin_name',
'URL': 'url',
'Unique ID': 'unique_id',
'Url': 'uri',
'Vulnerability Category': 'vulnerability_category',
'Virtual Host': 'virutal_host',
'WASC': 'wasc',
'Web Application Name': 'web_application_name'}
def __init__( def __init__(
self, self,
config=None, config=None,
@ -594,7 +570,7 @@ class vulnWhispererQualys(vulnWhispererBase):
debug=False, debug=False,
username=None, username=None,
password=None, password=None,
): ):
super(vulnWhispererQualys, self).__init__(config=config) super(vulnWhispererQualys, self).__init__(config=config)
self.logger = logging.getLogger('vulnWhispererQualys') self.logger = logging.getLogger('vulnWhispererQualys')
@ -632,7 +608,7 @@ class vulnWhispererQualys(vulnWhispererBase):
relative_path_name = self.path_check(report_name).encode('utf8') relative_path_name = self.path_check(report_name).encode('utf8')
if os.path.isfile(relative_path_name): if os.path.isfile(relative_path_name):
# TODO Possibly make this optional to sync directories #TODO Possibly make this optional to sync directories
file_length = len(open(relative_path_name).readlines()) file_length = len(open(relative_path_name).readlines())
record_meta = ( record_meta = (
scan_name, scan_name,
@ -684,7 +660,7 @@ class vulnWhispererQualys(vulnWhispererBase):
f.write('\n') f.write('\n')
elif output_format == 'csv': elif output_format == 'csv':
vuln_ready.to_csv(relative_path_name, index=False, header=True) # add when timestamp occured vuln_ready.to_csv(relative_path_name, index=False, header=True) # add when timestamp occured
self.logger.info('Report written to {}'.format(report_name)) self.logger.info('Report written to {}'.format(report_name))
@ -692,8 +668,7 @@ class vulnWhispererQualys(vulnWhispererBase):
self.logger.info('Removing report {} from Qualys Database'.format(generated_report_id)) self.logger.info('Removing report {} from Qualys Database'.format(generated_report_id))
cleaning_up = self.qualys_scan.qw.delete_report(generated_report_id) cleaning_up = self.qualys_scan.qw.delete_report(generated_report_id)
os.remove(self.path_check(str(generated_report_id) + '.csv')) os.remove(self.path_check(str(generated_report_id) + '.csv'))
self.logger.info( self.logger.info('Deleted report from local disk: {}'.format(self.path_check(str(generated_report_id))))
'Deleted report from local disk: {}'.format(self.path_check(str(generated_report_id))))
else: else:
self.logger.error('Could not process report ID: {}'.format(status)) self.logger.error('Could not process report ID: {}'.format(status))
@ -701,6 +676,7 @@ class vulnWhispererQualys(vulnWhispererBase):
self.logger.error('Could not process {}: {}'.format(report_id, str(e))) self.logger.error('Could not process {}: {}'.format(report_id, str(e)))
return vuln_ready return vuln_ready
def identify_scans_to_process(self): def identify_scans_to_process(self):
if self.uuids: if self.uuids:
self.scans_to_process = self.latest_scans[~self.latest_scans['id'].isin(self.uuids)] self.scans_to_process = self.latest_scans[~self.latest_scans['id'].isin(self.uuids)]
@ -708,6 +684,7 @@ class vulnWhispererQualys(vulnWhispererBase):
self.scans_to_process = self.latest_scans self.scans_to_process = self.latest_scans
self.logger.info('Identified {new} scans to be processed'.format(new=len(self.scans_to_process))) self.logger.info('Identified {new} scans to be processed'.format(new=len(self.scans_to_process)))
def process_web_assets(self): def process_web_assets(self):
counter = 0 counter = 0
self.identify_scans_to_process() self.identify_scans_to_process()
@ -788,6 +765,7 @@ class vulnWhispererOpenVAS(vulnWhispererBase):
if report_id: if report_id:
self.logger.info('Processing report ID: {}'.format(report_id)) self.logger.info('Processing report ID: {}'.format(report_id))
scan_name = report_id.replace('-', '') scan_name = report_id.replace('-', '')
report_name = 'openvas_scan_{scan_name}_{last_updated}.{extension}'.format(scan_name=scan_name, report_name = 'openvas_scan_{scan_name}_{last_updated}.{extension}'.format(scan_name=scan_name,
last_updated=launched_date, last_updated=launched_date,
@ -855,8 +833,7 @@ class vulnWhispererOpenVAS(vulnWhispererBase):
for scan in self.scans_to_process.iterrows(): for scan in self.scans_to_process.iterrows():
counter += 1 counter += 1
info = scan[1] info = scan[1]
self.logger.info( self.logger.info('Processing {}/{} - Report ID: {}'.format(counter, len(self.scans_to_process), info['report_ids']))
'Processing {}/{} - Report ID: {}'.format(counter, len(self.scans_to_process), info['report_ids']))
self.whisper_reports(report_id=info['report_ids'], self.whisper_reports(report_id=info['report_ids'],
launched_date=info['epoch']) launched_date=info['epoch'])
self.logger.info('Processing complete') self.logger.info('Processing complete')
@ -867,14 +844,15 @@ class vulnWhispererOpenVAS(vulnWhispererBase):
class vulnWhispererQualysVuln(vulnWhispererBase): class vulnWhispererQualysVuln(vulnWhispererBase):
CONFIG_SECTION = 'qualys_vuln' CONFIG_SECTION = 'qualys_vuln'
COLUMN_MAPPING = {'cvss_base': 'cvss', COLUMN_MAPPING = {'cvss_base': 'cvss',
'cvss3_base': 'cvss3', 'cvss3_base': 'cvss3',
'cve_id': 'cve', 'cve_id': 'cve',
'os': 'operating_system', 'os': 'operating_system',
'qid': 'plugin_id', 'qid': 'plugin_id',
'severity': 'risk', 'severity': 'risk',
'title': 'plugin_name'} 'title': 'plugin_name'}
def __init__( def __init__(
self, self,
@ -885,7 +863,7 @@ class vulnWhispererQualysVuln(vulnWhispererBase):
debug=False, debug=False,
username=None, username=None,
password=None, password=None,
): ):
super(vulnWhispererQualysVuln, self).__init__(config=config) super(vulnWhispererQualysVuln, self).__init__(config=config)
self.logger = logging.getLogger('vulnWhispererQualysVuln') self.logger = logging.getLogger('vulnWhispererQualysVuln')
@ -907,65 +885,66 @@ class vulnWhispererQualysVuln(vulnWhispererBase):
output_format='json', output_format='json',
cleanup=True): cleanup=True):
if 'Z' in launched_date: if 'Z' in launched_date:
launched_date = self.qualys_scan.utils.iso_to_epoch(launched_date) launched_date = self.qualys_scan.utils.iso_to_epoch(launched_date)
report_name = 'qualys_vuln_' + report_id.replace('/', '_') \ report_name = 'qualys_vuln_' + report_id.replace('/','_') \
+ '_{last_updated}'.format(last_updated=launched_date) \ + '_{last_updated}'.format(last_updated=launched_date) \
+ '.json' + '.json'
relative_path_name = self.path_check(report_name).encode('utf8') relative_path_name = self.path_check(report_name).encode('utf8')
if os.path.isfile(relative_path_name): if os.path.isfile(relative_path_name):
# TODO Possibly make this optional to sync directories #TODO Possibly make this optional to sync directories
file_length = len(open(relative_path_name).readlines()) file_length = len(open(relative_path_name).readlines())
record_meta = ( record_meta = (
scan_name, scan_name,
scan_reference, scan_reference,
launched_date, launched_date,
report_name, report_name,
time.time(), time.time(),
file_length, file_length,
self.CONFIG_SECTION, self.CONFIG_SECTION,
report_id, report_id,
1, 1,
0, 0,
) )
self.record_insert(record_meta) self.record_insert(record_meta)
self.logger.info('File {filename} already exist! Updating database'.format(filename=relative_path_name)) self.logger.info('File {filename} already exist! Updating database'.format(filename=relative_path_name))
else: else:
try: try:
self.logger.info('Processing report ID: {}'.format(report_id)) self.logger.info('Processing report ID: {}'.format(report_id))
vuln_ready = self.qualys_scan.process_data(scan_id=report_id) vuln_ready = self.qualys_scan.process_data(scan_id=report_id)
vuln_ready['scan_name'] = scan_name vuln_ready['scan_name'] = scan_name
vuln_ready['scan_reference'] = report_id vuln_ready['scan_reference'] = report_id
vuln_ready.rename(columns=self.COLUMN_MAPPING, inplace=True) vuln_ready.rename(columns=self.COLUMN_MAPPING, inplace=True)
except Exception as e: except Exception as e:
self.logger.error('Could not process {}: {}'.format(report_id, str(e))) self.logger.error('Could not process {}: {}'.format(report_id, str(e)))
self.exit_code += 1 self.exit_code += 1
return self.exit_code return self.exit_code
record_meta = ( record_meta = (
scan_name, scan_name,
scan_reference, scan_reference,
launched_date, launched_date,
report_name, report_name,
time.time(), time.time(),
vuln_ready.shape[0], vuln_ready.shape[0],
self.CONFIG_SECTION, self.CONFIG_SECTION,
report_id, report_id,
1, 1,
0, 0,
) )
self.record_insert(record_meta) self.record_insert(record_meta)
if output_format == 'json': if output_format == 'json':
with open(relative_path_name, 'w') as f: with open(relative_path_name, 'w') as f:
f.write(vuln_ready.to_json(orient='records', lines=True)) f.write(vuln_ready.to_json(orient='records', lines=True))
f.write('\n') f.write('\n')
self.logger.info('Report written to {}'.format(report_name))
return self.exit_code
self.logger.info('Report written to {}'.format(report_name))
return self.exit_code
def identify_scans_to_process(self): def identify_scans_to_process(self):
self.latest_scans = self.qualys_scan.qw.get_all_scans() self.latest_scans = self.qualys_scan.qw.get_all_scans()
@ -977,6 +956,7 @@ class vulnWhispererQualysVuln(vulnWhispererBase):
self.scans_to_process = self.latest_scans self.scans_to_process = self.latest_scans
self.logger.info('Identified {new} scans to be processed'.format(new=len(self.scans_to_process))) self.logger.info('Identified {new} scans to be processed'.format(new=len(self.scans_to_process)))
def process_vuln_scans(self): def process_vuln_scans(self):
counter = 0 counter = 0
self.identify_scans_to_process() self.identify_scans_to_process()
@ -986,9 +966,9 @@ class vulnWhispererQualysVuln(vulnWhispererBase):
r = app[1] r = app[1]
self.logger.info('Processing {}/{}'.format(counter, len(self.scans_to_process))) self.logger.info('Processing {}/{}'.format(counter, len(self.scans_to_process)))
self.exit_code += self.whisper_reports(report_id=r['id'], self.exit_code += self.whisper_reports(report_id=r['id'],
launched_date=r['date'], launched_date=r['date'],
scan_name=r['name'], scan_name=r['name'],
scan_reference=r['type']) scan_reference=r['type'])
else: else:
self.logger.info('No new scans to process. Exiting...') self.logger.info('No new scans to process. Exiting...')
self.conn.close() self.conn.close()
@ -996,6 +976,7 @@ class vulnWhispererQualysVuln(vulnWhispererBase):
class vulnWhispererJIRA(vulnWhispererBase): class vulnWhispererJIRA(vulnWhispererBase):
CONFIG_SECTION = 'jira' CONFIG_SECTION = 'jira'
def __init__( def __init__(
@ -1007,7 +988,7 @@ class vulnWhispererJIRA(vulnWhispererBase):
debug=False, debug=False,
username=None, username=None,
password=None, password=None,
): ):
super(vulnWhispererJIRA, self).__init__(config=config) super(vulnWhispererJIRA, self).__init__(config=config)
self.logger = logging.getLogger('vulnWhispererJira') self.logger = logging.getLogger('vulnWhispererJira')
if debug: if debug:
@ -1017,8 +998,8 @@ class vulnWhispererJIRA(vulnWhispererBase):
self.host_resolv_cache = {} self.host_resolv_cache = {}
self.host_no_resolv = [] self.host_no_resolv = []
self.no_resolv_by_team_dict = {} self.no_resolv_by_team_dict = {}
# Save locally those assets without DNS entry for flag to system owners #Save locally those assets without DNS entry for flag to system owners
self.no_resolv_fname = "no_resolv.txt" self.no_resolv_fname="no_resolv.txt"
if os.path.isfile(self.no_resolv_fname): if os.path.isfile(self.no_resolv_fname):
with open(self.no_resolv_fname, "r") as json_file: with open(self.no_resolv_fname, "r") as json_file:
self.no_resolv_by_team_dict = json.load(json_file) self.no_resolv_by_team_dict = json.load(json_file)
@ -1029,9 +1010,9 @@ class vulnWhispererJIRA(vulnWhispererBase):
self.logger.info('Attempting to connect to jira...') self.logger.info('Attempting to connect to jira...')
self.jira = \ self.jira = \
JiraAPI(hostname=self.hostname, JiraAPI(hostname=self.hostname,
username=self.username, username=self.username,
password=self.password, password=self.password,
path=self.config.get('jira', 'write_path')) path=self.config.get('jira','write_path'))
self.jira_connect = True self.jira_connect = True
self.logger.info('Connected to jira on {host}'.format(host=self.hostname)) self.logger.info('Connected to jira on {host}'.format(host=self.hostname))
except Exception as e: except Exception as e:
@ -1040,25 +1021,24 @@ class vulnWhispererJIRA(vulnWhispererBase):
'Could not connect to nessus -- Please verify your settings in {config} are correct and try again.\nReason: {e}'.format( 'Could not connect to nessus -- Please verify your settings in {config} are correct and try again.\nReason: {e}'.format(
config=self.config.config_in, e=e)) config=self.config.config_in, e=e))
return False return False
# sys.exit(1) #sys.exit(1)
profiles = [] profiles = []
profiles = self.get_scan_profiles() profiles = self.get_scan_profiles()
if not self.config.exists_jira_profiles(profiles): if not self.config.exists_jira_profiles(profiles):
self.config.update_jira_profiles(profiles) self.config.update_jira_profiles(profiles)
self.logger.info( self.logger.info("Jira profiles have been created in {config}, please fill the variables before rerunning the module.".format(config=self.config_path))
"Jira profiles have been created in {config}, please fill the variables before rerunning the module.".format(
config=self.config_path))
sys.exit(0) sys.exit(0)
def get_env_variables(self, source, scan_name): def get_env_variables(self, source, scan_name):
# function returns an array with [jira_project, jira_components, datafile_path] # function returns an array with [jira_project, jira_components, datafile_path]
# Jira variables #Jira variables
jira_section = self.config.normalize_section("{}.{}".format(source, scan_name)) jira_section = self.config.normalize_section("{}.{}".format(source,scan_name))
project = self.config.get(jira_section, 'jira_project') project = self.config.get(jira_section,'jira_project')
if project == "": if project == "":
self.logger.error('JIRA project is missing on the configuration file!') self.logger.error('JIRA project is missing on the configuration file!')
sys.exit(0) sys.exit(0)
@ -1068,39 +1048,35 @@ class vulnWhispererJIRA(vulnWhispererBase):
self.logger.error("JIRA project '{project}' doesn't exist!".format(project=project)) self.logger.error("JIRA project '{project}' doesn't exist!".format(project=project))
sys.exit(0) sys.exit(0)
components = self.config.get(jira_section, 'components').split(',') components = self.config.get(jira_section,'components').split(',')
# cleaning empty array from '' #cleaning empty array from ''
if not components[0]: if not components[0]:
components = [] components = []
min_critical = self.config.get(jira_section, 'min_critical_to_report') min_critical = self.config.get(jira_section,'min_critical_to_report')
if not min_critical: if not min_critical:
self.logger.error('"min_critical_to_report" variable on config file is empty.') self.logger.error('"min_critical_to_report" variable on config file is empty.')
sys.exit(0) sys.exit(0)
# datafile path #datafile path
filename, reported = self.get_latest_results(source, scan_name) filename, reported = self.get_latest_results(source, scan_name)
fullpath = "" fullpath = ""
# search data files under user specified directory # search data files under user specified directory
for root, dirnames, filenames in os.walk(vwConfig(self.config_path).get(source, 'write_path')): for root, dirnames, filenames in os.walk(vwConfig(self.config_path).get(source,'write_path')):
if filename in filenames: if filename in filenames:
fullpath = "{}/{}".format(root, filename) fullpath = "{}/{}".format(root,filename)
if reported: if reported:
self.logger.warn( self.logger.warn('Last Scan of "{scan_name}" for source "{source}" has already been reported; will be skipped.'.format(scan_name=scan_name, source=source))
'Last Scan of "{scan_name}" for source "{source}" has already been reported; will be skipped.'.format(
scan_name=scan_name, source=source))
return [False] * 5 return [False] * 5
if not fullpath: if not fullpath:
self.logger.error( self.logger.error('Scan of "{scan_name}" for source "{source}" has not been found. Please check that the scanner data files are in place.'.format(scan_name=scan_name, source=source))
'Scan of "{scan_name}" for source "{source}" has not been found. Please check that the scanner data files are in place.'.format(
scan_name=scan_name, source=source))
sys.exit(1) sys.exit(1)
dns_resolv = self.config.get('jira', 'dns_resolv') dns_resolv = self.config.get('jira','dns_resolv')
if dns_resolv in ('False', 'false', ''): if dns_resolv in ('False', 'false', ''):
dns_resolv = False dns_resolv = False
elif dns_resolv in ('True', 'true'): elif dns_resolv in ('True', 'true'):
@ -1111,36 +1087,36 @@ class vulnWhispererJIRA(vulnWhispererBase):
return project, components, fullpath, min_critical, dns_resolv return project, components, fullpath, min_critical, dns_resolv
def parse_nessus_vulnerabilities(self, fullpath, source, scan_name, min_critical): def parse_nessus_vulnerabilities(self, fullpath, source, scan_name, min_critical):
vulnerabilities = [] vulnerabilities = []
# we need to parse the CSV # we need to parse the CSV
risks = ['none', 'low', 'medium', 'high', 'critical'] risks = ['none', 'low', 'medium', 'high', 'critical']
min_risk = int([i for i, x in enumerate(risks) if x == min_critical][0]) min_risk = int([i for i,x in enumerate(risks) if x == min_critical][0])
df = pd.read_csv(fullpath, delimiter=',') df = pd.read_csv(fullpath, delimiter=',')
# nessus fields we want - ['Host','Protocol','Port', 'Name', 'Synopsis', 'Description', 'Solution', 'See Also'] #nessus fields we want - ['Host','Protocol','Port', 'Name', 'Synopsis', 'Description', 'Solution', 'See Also']
for index in range(len(df)): for index in range(len(df)):
# filtering vulnerabilities by criticality, discarding low risk # filtering vulnerabilities by criticality, discarding low risk
to_report = int([i for i, x in enumerate(risks) if x == df.loc[index]['Risk'].lower()][0]) to_report = int([i for i,x in enumerate(risks) if x == df.loc[index]['Risk'].lower()][0])
if to_report < min_risk: if to_report < min_risk:
continue continue
if not vulnerabilities or df.loc[index]['Name'] not in [entry['title'] for entry in vulnerabilities]: if not vulnerabilities or df.loc[index]['Name'] not in [entry['title'] for entry in vulnerabilities]:
vuln = {} vuln = {}
# vulnerabilities should have all the info for creating all JIRA labels #vulnerabilities should have all the info for creating all JIRA labels
vuln['source'] = source vuln['source'] = source
vuln['scan_name'] = scan_name vuln['scan_name'] = scan_name
# vulnerability variables #vulnerability variables
vuln['title'] = df.loc[index]['Name'] vuln['title'] = df.loc[index]['Name']
vuln['diagnosis'] = df.loc[index]['Synopsis'].replace('\\n', ' ') vuln['diagnosis'] = df.loc[index]['Synopsis'].replace('\\n',' ')
vuln['consequence'] = df.loc[index]['Description'].replace('\\n', ' ') vuln['consequence'] = df.loc[index]['Description'].replace('\\n',' ')
vuln['solution'] = df.loc[index]['Solution'].replace('\\n', ' ') vuln['solution'] = df.loc[index]['Solution'].replace('\\n',' ')
vuln['ips'] = [] vuln['ips'] = []
vuln['ips'].append( vuln['ips'].append("{} - {}/{}".format(df.loc[index]['Host'], df.loc[index]['Protocol'], df.loc[index]['Port']))
"{} - {}/{}".format(df.loc[index]['Host'], df.loc[index]['Protocol'], df.loc[index]['Port']))
vuln['risk'] = df.loc[index]['Risk'].lower() vuln['risk'] = df.loc[index]['Risk'].lower()
# Nessus "nan" value gets automatically casted to float by python # Nessus "nan" value gets automatically casted to float by python
@ -1154,54 +1130,51 @@ class vulnWhispererJIRA(vulnWhispererBase):
# grouping assets by vulnerability to open on single ticket, as each asset has its own nessus entry # grouping assets by vulnerability to open on single ticket, as each asset has its own nessus entry
for vuln in vulnerabilities: for vuln in vulnerabilities:
if vuln['title'] == df.loc[index]['Name']: if vuln['title'] == df.loc[index]['Name']:
vuln['ips'].append("{} - {}/{}".format(df.loc[index]['Host'], df.loc[index]['Protocol'], vuln['ips'].append("{} - {}/{}".format(df.loc[index]['Host'], df.loc[index]['Protocol'], df.loc[index]['Port']))
df.loc[index]['Port']))
return vulnerabilities return vulnerabilities
def parse_qualys_vuln_vulnerabilities(self, fullpath, source, scan_name, min_critical, dns_resolv=False): def parse_qualys_vuln_vulnerabilities(self, fullpath, source, scan_name, min_critical, dns_resolv = False):
# parsing of the qualys vulnerabilities schema #parsing of the qualys vulnerabilities schema
# parse json #parse json
vulnerabilities = [] vulnerabilities = []
risks = ['info', 'low', 'medium', 'high', 'critical'] risks = ['info', 'low', 'medium', 'high', 'critical']
# +1 as array is 0-4, but score is 1-5 # +1 as array is 0-4, but score is 1-5
min_risk = int([i for i, x in enumerate(risks) if x == min_critical][0]) + 1 min_risk = int([i for i,x in enumerate(risks) if x == min_critical][0])+1
try: try:
data = [json.loads(line) for line in open(fullpath).readlines()] data=[json.loads(line) for line in open(fullpath).readlines()]
except Exception as e: except Exception as e:
self.logger.warn("Scan has no vulnerabilities, skipping.") self.logger.warn("Scan has no vulnerabilities, skipping.")
return vulnerabilities return vulnerabilities
# qualys fields we want - [] #qualys fields we want - []
for index in range(len(data)): for index in range(len(data)):
if int(data[index]['risk']) < min_risk: if int(data[index]['risk']) < min_risk:
continue continue
elif data[index]['type'] == 'Practice' or data[index]['type'] == 'Ig': elif data[index]['type'] == 'Practice' or data[index]['type'] == 'Ig':
self.logger.debug("Vulnerability '{vuln}' ignored, as it is 'Practice/Potential', not verified.".format( self.logger.debug("Vulnerability '{vuln}' ignored, as it is 'Practice/Potential', not verified.".format(vuln=data[index]['plugin_name']))
vuln=data[index]['plugin_name']))
continue continue
if not vulnerabilities or data[index]['plugin_name'] not in [entry['title'] for entry in vulnerabilities]: if not vulnerabilities or data[index]['plugin_name'] not in [entry['title'] for entry in vulnerabilities]:
vuln = {} vuln = {}
# vulnerabilities should have all the info for creating all JIRA labels #vulnerabilities should have all the info for creating all JIRA labels
vuln['source'] = source vuln['source'] = source
vuln['scan_name'] = scan_name vuln['scan_name'] = scan_name
# vulnerability variables #vulnerability variables
vuln['title'] = data[index]['plugin_name'] vuln['title'] = data[index]['plugin_name']
vuln['diagnosis'] = data[index]['threat'].replace('\\n', ' ') vuln['diagnosis'] = data[index]['threat'].replace('\\n',' ')
vuln['consequence'] = data[index]['impact'].replace('\\n', ' ') vuln['consequence'] = data[index]['impact'].replace('\\n',' ')
vuln['solution'] = data[index]['solution'].replace('\\n', ' ') vuln['solution'] = data[index]['solution'].replace('\\n',' ')
vuln['ips'] = [] vuln['ips'] = []
# TODO ADDED DNS RESOLUTION FROM QUALYS! \n SEPARATORS INSTEAD OF \\n! #TODO ADDED DNS RESOLUTION FROM QUALYS! \n SEPARATORS INSTEAD OF \\n!
vuln['ips'].append( vuln['ips'].append("{ip} - {protocol}/{port} - {dns}".format(**self.get_asset_fields(data[index], dns_resolv)))
"{ip} - {protocol}/{port} - {dns}".format(**self.get_asset_fields(data[index], dns_resolv)))
# different risk system than Nessus! #different risk system than Nessus!
vuln['risk'] = risks[int(data[index]['risk']) - 1] vuln['risk'] = risks[int(data[index]['risk'])-1]
# Nessus "nan" value gets automatically casted to float by python # Nessus "nan" value gets automatically casted to float by python
if not (type(data[index]['vendor_reference']) is float or data[index]['vendor_reference'] == None): if not (type(data[index]['vendor_reference']) is float or data[index]['vendor_reference'] == None):
@ -1213,8 +1186,7 @@ class vulnWhispererJIRA(vulnWhispererBase):
# grouping assets by vulnerability to open on single ticket, as each asset has its own nessus entry # grouping assets by vulnerability to open on single ticket, as each asset has its own nessus entry
for vuln in vulnerabilities: for vuln in vulnerabilities:
if vuln['title'] == data[index]['plugin_name']: if vuln['title'] == data[index]['plugin_name']:
vuln['ips'].append( vuln['ips'].append("{ip} - {protocol}/{port} - {dns}".format(**self.get_asset_fields(data[index], dns_resolv)))
"{ip} - {protocol}/{port} - {dns}".format(**self.get_asset_fields(data[index], dns_resolv)))
return vulnerabilities return vulnerabilities
@ -1228,7 +1200,7 @@ class vulnWhispererJIRA(vulnWhispererBase):
if vuln['dns']: if vuln['dns']:
values['dns'] = vuln['dns'] values['dns'] = vuln['dns']
else: else:
if values['ip'] in list(self.host_resolv_cache.keys()): if values['ip'] in self.host_resolv_cache.keys():
self.logger.debug("Hostname from {ip} cached, retrieving from cache.".format(ip=values['ip'])) self.logger.debug("Hostname from {ip} cached, retrieving from cache.".format(ip=values['ip']))
values['dns'] = self.host_resolv_cache[values['ip']] values['dns'] = self.host_resolv_cache[values['ip']]
else: else:
@ -1249,54 +1221,54 @@ class vulnWhispererJIRA(vulnWhispererBase):
return values return values
def parse_vulnerabilities(self, fullpath, source, scan_name, min_critical): def parse_vulnerabilities(self, fullpath, source, scan_name, min_critical):
# TODO: SINGLE LOCAL SAVE FORMAT FOR ALL SCANNERS #TODO: SINGLE LOCAL SAVE FORMAT FOR ALL SCANNERS
# JIRA standard vuln format - ['source', 'scan_name', 'title', 'diagnosis', 'consequence', 'solution', 'ips', 'references'] #JIRA standard vuln format - ['source', 'scan_name', 'title', 'diagnosis', 'consequence', 'solution', 'ips', 'references']
return 0 return 0
def jira_sync(self, source, scan_name): def jira_sync(self, source, scan_name):
self.logger.info("Jira Sync triggered for source '{source}' and scan '{scan_name}'".format(source=source, self.logger.info("Jira Sync triggered for source '{source}' and scan '{scan_name}'".format(source=source, scan_name=scan_name))
scan_name=scan_name))
project, components, fullpath, min_critical, dns_resolv = self.get_env_variables(source, scan_name) project, components, fullpath, min_critical, dns_resolv = self.get_env_variables(source, scan_name)
if not project: if not project:
self.logger.debug( self.logger.debug("Skipping scan for source '{source}' and scan '{scan_name}': vulnerabilities have already been reported.".format(source=source, scan_name=scan_name))
"Skipping scan for source '{source}' and scan '{scan_name}': vulnerabilities have already been reported.".format(
source=source, scan_name=scan_name))
return False return False
vulnerabilities = [] vulnerabilities = []
# ***Nessus parsing*** #***Nessus parsing***
if source == "nessus": if source == "nessus":
vulnerabilities = self.parse_nessus_vulnerabilities(fullpath, source, scan_name, min_critical) vulnerabilities = self.parse_nessus_vulnerabilities(fullpath, source, scan_name, min_critical)
# ***Qualys VM parsing*** #***Qualys VM parsing***
if source == "qualys_vuln": if source == "qualys_vuln":
vulnerabilities = self.parse_qualys_vuln_vulnerabilities(fullpath, source, scan_name, min_critical, vulnerabilities = self.parse_qualys_vuln_vulnerabilities(fullpath, source, scan_name, min_critical, dns_resolv)
dns_resolv)
# ***JIRA sync*** #***JIRA sync***
if vulnerabilities: try:
self.logger.info('{source} data has been successfuly parsed'.format(source=source.upper())) if vulnerabilities:
self.logger.info('Starting JIRA sync') self.logger.info('{source} data has been successfuly parsed'.format(source=source.upper()))
self.logger.info('Starting JIRA sync')
self.jira.sync(vulnerabilities, project, components) self.jira.sync(vulnerabilities, project, components)
else: else:
self.logger.info( self.logger.info("[{source}.{scan_name}] No vulnerabilities or vulnerabilities not parsed.".format(source=source, scan_name=scan_name))
"[{source}.{scan_name}] No vulnerabilities or vulnerabilities not parsed.".format(source=source, self.set_latest_scan_reported(fullpath.split("/")[-1])
scan_name=scan_name)) return False
self.set_latest_scan_reported(fullpath.split("/")[-1]) except Exception as e:
self.logger.error("Error: {}".format(e))
return False return False
# writing to file those assets without DNS resolution
# if its not empty #writing to file those assets without DNS resolution
#if its not empty
if self.host_no_resolv: if self.host_no_resolv:
# we will replace old list of non resolved for the new one or create if it doesn't exist already #we will replace old list of non resolved for the new one or create if it doesn't exist already
self.no_resolv_by_team_dict[scan_name] = self.host_no_resolv self.no_resolv_by_team_dict[scan_name] = self.host_no_resolv
with open(self.no_resolv_fname, 'w') as outfile: with open(self.no_resolv_fname, 'w') as outfile:
json.dump(self.no_resolv_by_team_dict, outfile) json.dump(self.no_resolv_by_team_dict, outfile)
self.set_latest_scan_reported(fullpath.split("/")[-1]) self.set_latest_scan_reported(fullpath.split("/")[-1])
return True return True
@ -1310,12 +1282,12 @@ class vulnWhispererJIRA(vulnWhispererBase):
self.jira_sync(self.config.get(scan, 'source'), self.config.get(scan, 'scan_name')) self.jira_sync(self.config.get(scan, 'source'), self.config.get(scan, 'scan_name'))
except Exception as e: except Exception as e:
self.logger.error( self.logger.error(
"VulnWhisperer wasn't able to report the vulnerabilities from the '{}'s source".format( "VulnWhisperer wasn't able to report the vulnerabilities from the '{}'s source, section {}.\
self.config.get(scan, 'source'))) \nError: {}".format(
self.config.get(scan, 'source'), self.config.get(scan, 'scan_name'), e))
return True return True
return False return False
class vulnWhisperer(object): class vulnWhisperer(object):
def __init__(self, def __init__(self,
@ -1339,6 +1311,7 @@ class vulnWhisperer(object):
self.scanname = scanname self.scanname = scanname
self.exit_code = 0 self.exit_code = 0
def whisper_vulnerabilities(self): def whisper_vulnerabilities(self):
if self.profile == 'nessus': if self.profile == 'nessus':
@ -1353,9 +1326,9 @@ class vulnWhisperer(object):
self.exit_code += vw.process_web_assets() self.exit_code += vw.process_web_assets()
elif self.profile == 'openvas': elif self.profile == 'openvas':
vw_openvas = vulnWhispererOpenVAS(config=self.config) vw = vulnWhispererOpenVAS(config=self.config)
if vw: if vw:
self.exit_code += vw_openvas.process_openvas_scans() self.exit_code += vw.process_openvas_scans()
elif self.profile == 'tenable': elif self.profile == 'tenable':
vw = vulnWhispererNessus(config=self.config, vw = vulnWhispererNessus(config=self.config,
@ -1369,7 +1342,7 @@ class vulnWhisperer(object):
self.exit_code += vw.process_vuln_scans() self.exit_code += vw.process_vuln_scans()
elif self.profile == 'jira': elif self.profile == 'jira':
# first we check config fields are created, otherwise we create them #first we check config fields are created, otherwise we create them
vw = vulnWhispererJIRA(config=self.config) vw = vulnWhispererJIRA(config=self.config)
if vw: if vw:
if not (self.source and self.scanname): if not (self.source and self.scanname):