#!/usr/bin/env python """ Gitlab bridge for PenText: imports and updates gitlab issues into PenText (XML) format Copyright (C) 2016 Peter Mosmans [Radically Open Security] This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. """ from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals import argparse import collections import os import re import sys import textwrap try: import gitlab import jxmlease # path to docbuilder installation (needs it module) sys.path.append('r:/public/docbuilder') import validate_report except ImportError: print('[-] This script needs gitlab, jxmlease and validate_report library', file=sys.stderr) sys.exit(-1) def add_finding(issue, options): title = validate_report.capitalize(issue.title.strip()) print_status('{0} - {1} - {2}'.format(issue.state, issue.labels, title), options) threatLevel = 'Moderate' finding_type = 'TODO' finding_id = '{0}-{1}'.format(issue.iid, valid_filename(title)) filename = 'findings/{0}.xml'.format(finding_id) finding = collections.OrderedDict() finding['title'] = title finding['description'] = unicode.replace(issue.description, '\r\n', '\n') finding['technicaldescription'] = '' for note in [x for x in issue.notes.list() if not x.system]: finding['technicaldescription'] += unicode.replace(note.body, '\r\n', '\n') finding['impact'] = {} finding['impact']['p'] = 'TODO' finding['recommendation'] = {} finding['recommendation']['ul'] = {} finding['recommendation']['ul']['li'] = 'TODO' finding_xml = jxmlease.XMLDictNode(finding, tag='finding', xml_attrs={'id': finding_id, 'threatLevel': threatLevel, 'type': finding_type}) if options['dry_run']: print_line('[+] {0}'.format(filename)) print(finding_xml.emit_xml()) 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_xml.emit_xml().encode('utf-8')) print_line('[+] Created {0}'.format(filename)) 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 = '{0}-{1}'.format(issue.iid, valid_filename(title)) filename = 'non-findings/{0}.xml'.format(non_finding_id) non_finding = collections.OrderedDict() non_finding['title'] = title non_finding['p'] = unicode.replace(issue.description, '\r\n', '\n') for note in [x for x in issue.notes.list() if not x.system]: non_finding['p'] += unicode.replace(note.body, '\r\n', '\n') non_finding_xml = jxmlease.XMLDictNode(non_finding, tag='non-finding', xml_attrs={'id': non_finding_id}) if options['dry_run']: print_line('[+] {0}'.format(filename)) print(non_finding_xml.emit_xml()) 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_xml.emit_xml().encode('utf-8')) print_line('[+] Created {0}'.format(filename)) def ask_permission(question): """ Ask question and return True if user answered with y. """ print_line('{0} ? [y/N]'.format(question)) return raw_input().lower() == 'y' def convert_markdown(text): """ Replace markdown monospace with monospace tags """ result = text return result print('EXAMINING ' + text + ' END') monospace = re.findall("\`\`\`(.*?)\`\`\`", text, re.DOTALL) print(monospace) if len(monospace): print('YESSS ' + monospace) result = {} result['monospace'] = ''.join(monospace) def list_issues(gitserver, options): """ Lists all issues for options['issues'] """ for issue in gitserver.projects.get(options['issues']).issues.list(all=True): if issue.state != 'opened' and not options['closed']: continue if 'finding' in issue.labels: add_finding(issue, options) if 'non-finding' in issue.labels: add_non_finding(issue, options) def list_projects(gitserver, options): """ Lists all available projects. """ for project in gitserver.projects.list(all=True): print_line('{0} - {1}'.format(project.as_dict()['id'], project.as_dict()['path'])) def parse_arguments(): """ Parses command line arguments. """ parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent('''\ gitlab-to-pentext - imports and updates gitlab issues into PetText (XML) format Copyright (C) 2016 Peter Mosmans [Radically Open Security]] This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.''')) parser.add_argument('--closed', action='store', help='take closed issues into account') parser.add_argument('--dry-run', action='store_true', help='do not write anything, only output on screen') parser.add_argument('--issues', action='store', help='list issues for a given project') parser.add_argument('--overwrite', action='store_true', help='overwrite existing issues') parser.add_argument('--projects', action='store_true', help='list gitlab projects') parser.add_argument('-v', '--verbose', action='store_true', help='increase output verbosity') parser.add_argument('-y', action='store_true', help='assume yes on all questions, write findings') if len(sys.argv) == 1: parser.print_help() return vars(parser.parse_args()) def preflight_checks(options): """ Checks if all tools are there. Exits with 0 if everything went okilydokily. """ try: gitserver = gitlab.Gitlab.from_config('remote') gitserver.auth() except gitlab.config.GitlabDataError as e: print_error('could not connect {0}'.format(e), -1) return gitserver def print_error(text, result=False): """ Prints error message. When @result, exits with result. """ if len(text): print_line('[-] ' + text, True) if result: sys.exit(result) def print_line(text, error=False): """ Prints text, and flushes stdout and stdin. When @error, prints text to stderr instead of stdout. """ if not error: print(text) else: print(text, file=sys.stderr) sys.stdout.flush() sys.stderr.flush() def print_status(text, options=False): """ Prints status message if options array is given and contains 'verbose'. """ if options and options['verbose']: print_line('[*] ' + str(text)) def valid_filename(filename): """ Return a valid filename. """ valid_filename = '' for char in filename.strip(): if char in [':', '/', '.', '\\', ' ', '[', ']', '(', ')', '\'']: if len(char) and not valid_filename.endswith('-'): valid_filename += '-' else: valid_filename += char return valid_filename.lower() def main(): """ The main program. """ options = parse_arguments() gitserver = preflight_checks(options) if options['projects']: list_projects(gitserver, options) if options['issues']: list_issues(gitserver, options) if __name__ == "__main__": main()