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 = '{0}>\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}{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 = '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: