Implemented true Unicode (UTF-8) support

Added first Pentext classes: Finding and NonFinding
This commit is contained in:
Peter Mosmans 2017-02-23 18:43:57 +11:00
parent feb154554a
commit acc6dc4b50

View File

@ -24,6 +24,7 @@ from __future__ import print_function
from __future__ import unicode_literals
import argparse
import io
import os
import sys
import textwrap
@ -34,7 +35,7 @@ try:
# Path of this script. The validate_report module is on the same path.
sys.path.append(os.path.dirname(__file__))
import validate_report
except ImportError as exception:
except (NameError, ImportError) as exception:
print('[-] This script needs python-gitlab, pypandoc and validate_report library',
file=sys.stderr)
print("validate_report is part of the pentext framework", file=sys.stderr)
@ -43,51 +44,135 @@ except ImportError as exception:
print("Currently missing: " + exception.message, file=sys.stderr)
sys.exit(-1)
class BaseItem(object):
"""
Base class for Pentext items
"""
DECLARATION = '<?xml version="1.0" encoding="utf-8"?>\n'
def add_finding(issue, options):
def __init__(self, item_type):
if item_type not in ('finding', 'non-finding'):
raise ValueError('Only finding and non-finding are currently supported')
self.item_type = item_type
self.__path = '{0}s'.format(self.item_type)
self.root_open = '<{0}>\n'.format(self.item_type)
self.root_close = '</{0}>\n'.format(self.item_type)
self.title = ''
self.content = ''
@property
def filename(self):
"""
Writes issue as XML finding to file.
Filename.
"""
title = validate_report.capitalize(issue.title.strip())
print_status('{0} - {1} - {2}'.format(issue.state, issue.labels,
title), options)
threat_level = 'Moderate'
finding_type = 'TODO'
finding_id = 'f{0}-{1}'.format(issue.iid, valid_filename(title))
filename = 'findings/{0}.xml'.format(finding_id)
finding = u'<title>{0}</title>\n'.format(title)
finding += '<description>{0}\n</description>\n'.format(convert_text(issue.description))
impact = 'TODO'
recommendation = '<ul>\n<li>\nTODO\n</li>\n</ul>\n';
technical_description = ''
return '{0}/{1}.xml'.format(self.__path, valid_filename(self.identifier))
def __str__(self):
"""
Return a XML version of the class
"""
return self.DECLARATION + self.root_open + self.element('title') + \
self.content + self.root_close
def element(self, attribute):
"""
Return opening and closing attribute tags, including attribute value.
"""
return '<{0}>{1}</{0}>\n'.format(attribute, getattr(self, attribute))
def write_file(self):
"""
Write item as XML to file.
"""
try:
with io.open(self.filename, 'w') as xmlfile:
xmlfile.write(unicode(self))
print_line('[+] Wrote {0}'.format(self.filename))
except IOError:
print_error('Could not write to %s', self.filename)
class Finding(BaseItem):
"""
Encapsulates finding.
"""
def __init__(self):
BaseItem.__init__(self, 'finding')
self.threat_level = 'Moderate'
self.finding_type = 'TODO'
self.description = '<p>TODO</p>'
self.technicaldescription = '<p>TODO</p>'
self.impact = '<p>TODO</p>'
self.recommendation = '<ul><li>TODO</li></ul>'
def __str__(self):
"""
Return a XML version of the class
"""
self.root_open = '<finding id="{0}" threatLevel="{1}" type="{2}">\n'.format(self.identifier,
self.threat_level,
self.finding_type)
self.content = self.element('description') + \
self.element('technicaldescription') + \
self.element('impact') + \
self.element('recommendation')
return BaseItem.__str__(self)
class NonFinding(BaseItem):
"""
Encapsulates non-finding.
"""
def __init__(self):
BaseItem.__init__(self, 'non-finding')
def from_issue(issue):
"""
Parse gitlab issue and return Finding, NonFinding or None
"""
if 'finding' in [x.lower() for x in issue.labels]:
item = Finding()
item.description = convert_text(issue.description)
for note in [x for x in reversed(issue.notes.list()) if not x.system]:
if len(note.body.splitlines()):
if 'impact' in note.body.split()[0].lower():
impact = convert_text(''.join(note.body.splitlines(True)[1:]))
item.impact = convert_text(''.join(note.body.splitlines(True)[1:]))
elif 'recommendation' in note.body.split()[0].lower():
recommendation = convert_text(''.join(note.body.splitlines(True)[1:]))
item.recommendation = convert_text(''.join(note.body.splitlines(True)[1:]))
else:
technical_description += u'{0}\n'.format(convert_text(note.body))
finding += '<technicaldescription>\n{0}\n</technicaldescription>\n\n'.format(technical_description)
finding += '<impact>\n{0}\n</impact>\n\n'.format(impact)
finding += '<recommendation>\n{0}\n</recommendation>\n\n'.format(recommendation)
finding = u'{0}<finding id="{1}" threatLevel="{2}" type="{3}">\n{4}</finding>'.format(DECLARATION,
finding_id,
threat_level,
finding_type,
finding)
item.technicaldescription += u'{0}\n'.format(convert_text(note.body))
elif 'non-finding' in [x.lower() for x in issue.labels]:
item = NonFinding()
for note in [x for x in reversed(issue.notes.list()) if not x.system]:
item.content += convert_text(note.body) + '\n'
else:
return None
item.title = validate_report.capitalize(issue.title.strip())
item.identifier = 'f{0}-{1}'.format(issue.iid, valid_filename(item.title))
return item
def add_item(issue, options):
"""
Convert issue into XML finding and create file.
"""
item = from_issue(issue)
if not item:
return
if os.path.isfile(item.filename) and not options['overwrite']:
print_line('{0} {1} already exists (use --overwrite to overwrite)'.
format(item.item_type, item.filename))
return
if options['dry_run']:
print_line('[+] {0}\n{1}'.format(filename, finding))
print_line('[+] {0}\n{1}'.format(item.filename, item))
else:
if os.path.isfile(filename) and not options['overwrite']:
print_line('Finding {0} already exists (use --overwrite to overwrite)'.
format(filename))
else:
if options['y'] or ask_permission('Create file ' + filename):
with open(filename, 'w') as xmlfile:
xmlfile.write(finding)
print_line('[+] Created {0}'.format(filename))
if options['y'] or ask_permission('Create file ' + item.filename):
item.write_file()
def convert_text(text):
@ -97,35 +182,6 @@ def convert_text(text):
return unicode.replace(pypandoc.convert_text(text, 'html5', format='markdown_github'), '\r\n', '\n')
def add_non_finding(issue, options):
"""
Adds a non-finding.
"""
title = validate_report.capitalize(issue.title.strip())
print_status('{0} - {1} - {2}'.format(issue.state, issue.labels,
title), options)
non_finding_id = 'nf{0}-{1}'.format(issue.iid, valid_filename(title))
filename = 'non-findings/{0}.xml'.format(non_finding_id)
non_finding = u'<title>{0}</title>\n{1}\n'.format(title,
convert_text(issue.description))
for note in [x for x in reversed(issue.notes.list()) if not x.system]:
non_finding += u'<p>{0}</p>\n'.format(convert_text(note.body))
non_finding = u'{0}<non-finding id="{1}">\n{2}\n</non-finding>\n'.format(DECLARATION,
non_finding_id,
non_finding)
if options['dry_run']:
print_line('[+] {0}\n{1}'.format(filename, non_finding))
else:
if os.path.isfile(filename) and not options['overwrite']:
print_line('Non-finding {0} already exists (use --overwrite to overwrite)'.
format(filename))
else:
if options['y'] or ask_permission('Create file ' + filename):
with open(filename, 'w') as xmlfile:
xmlfile.write(non_finding)
print_line('[+] Created {0}'.format(filename))
def ask_permission(question):
"""
Ask question and return True if user answered with y.
@ -138,17 +194,11 @@ def list_issues(gitserver, options):
"""
Lists all issues for options['issues']
"""
try:
for issue in gitserver.project_issues.list(project_id=options['issues'],
per_page=999):
if issue.state == 'closed' and not options['closed']:
continue
if 'finding' in [x.lower() for x in issue.labels]:
add_finding(issue, options)
if 'non-finding' in [x.lower() for x in issue.labels]:
add_non_finding(issue, options)
except Exception as exception:
print_error('could not find any issues ({0})'.format(exception), -1)
add_item(issue, options)
def list_projects(gitserver):
@ -205,6 +255,9 @@ def preflight_checks():
gitserver.auth()
except gitlab.config.GitlabDataError as exception:
print_error('could not connect {0}'.format(exception), -1)
for path in ('findings', 'non-findings'):
if not os.path.isdir(path):
print_error('Path {0} does not exist: Is this a Pentext repository ?'.format(path), -1)
return gitserver
@ -246,7 +299,7 @@ def valid_filename(filename):
"""
result = ''
for char in filename.strip():
if char in ['*', ':', '/', '.', '\\', ' ', '[', ']', '(', ')', '\'']:
if char in ['*', ':', '/', '.', '\\', ' ', '[', ']', '(', ')', '\'', '\"']:
if len(char) and not result.endswith('-'):
result += '-'
else: