diff --git a/chatops/python/gitlab-to-pentext.py b/chatops/python/gitlab-to-pentext.py index d246d30..b5eff5a 100644 --- a/chatops/python/gitlab-to-pentext.py +++ b/chatops/python/gitlab-to-pentext.py @@ -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) -DECLARATION = '\n' - -def add_finding(issue, options): + +class BaseItem(object): """ - Writes issue as XML finding to file. + Base class for Pentext items """ - 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'{0}\n'.format(title) - finding += '{0}\n\n'.format(convert_text(issue.description)) - impact = 'TODO' - recommendation = '\n'; - technical_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:])) - elif 'recommendation' in note.body.split()[0].lower(): - recommendation = convert_text(''.join(note.body.splitlines(True)[1:])) - else: - technical_description += u'{0}\n'.format(convert_text(note.body)) - finding += '\n{0}\n\n\n'.format(technical_description) - finding += '\n{0}\n\n\n'.format(impact) - finding += '\n{0}\n\n\n'.format(recommendation) - finding = u'{0}\n{4}'.format(DECLARATION, - finding_id, - threat_level, - finding_type, - finding) - if options['dry_run']: - print_line('[+] {0}\n{1}'.format(filename, finding)) + + DECLARATION = '\n' + + 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 = '\n'.format(self.item_type) + self.title = '' + self.content = '' + + @property + def filename(self): + """ + Filename. + """ + 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}\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 = '

TODO

' + self.technicaldescription = '

TODO

' + self.impact = '

TODO

' + self.recommendation = '' + + def __str__(self): + """ + Return a XML version of the class + """ + self.root_open = '\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(): + item.impact = convert_text(''.join(note.body.splitlines(True)[1:])) + elif 'recommendation' in note.body.split()[0].lower(): + item.recommendation = convert_text(''.join(note.body.splitlines(True)[1:])) + else: + 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: - 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)) + 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(item.filename, item)) + else: + 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'{0}\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'

{0}

\n'.format(convert_text(note.body)) - non_finding = u'{0}\n{2}\n\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) + for issue in gitserver.project_issues.list(project_id=options['issues'], + per_page=999): + if issue.state == 'closed' and not options['closed']: + continue + 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: