from configparser import ConfigParser import argparse import os import re import sys import bugzilla import requests from bs4 import BeautifulSoup as bs from CVETool import CVETool GLSAMAKER_URI = 'https://glsamaker.gentoo.org' class GLSATool: """ Utility to ease GLSA handling in GLSAMaker """ def __init__(self, glsamaker_key, bgo_key): self.auth = glsamaker_key self.bgo = bugzilla.Bugzilla('https://bugs.gentoo.org', api_key=bgo_key, force_rest=True) config_path = os.path.join(os.path.expanduser('~'), '.config', 'glsatool') c = ConfigParser() c.read(config_path) self.glsa_path = c['default']['glsa'] self.from_str = c['default']['from'] def get_csrf_token(self, path): soup = bs(self.request(path), features='lxml') csrf_token = \ soup.find('input', {'name': 'authenticity_token'})['value'] return csrf_token def request(self, path, method='GET', data=None): if method == 'GET': response = requests.get(GLSAMAKER_URI + path, headers={'Authorization': 'Basic ' + self.auth}) elif method == 'POST': if data: response = requests.post(GLSAMAKER_URI + path, data=data, headers={'Authorization': 'Basic ' + self.auth}) else: response = requests.post(GLSAMAKER_URI + path, headers={'Authorization': 'Basic ' + self.auth}) if not response.ok: raise RuntimeError(path + ': ' + str(response.status_code)) return response.text def get_int_input(self, msg): while True: i = input(msg) try: return int(i) except (ValueError, EOFError): continue def release_glsa(self, num=None): if not num: soup = bs(self.request('/glsas/drafts'), features='lxml') glsas = soup.find_all('tr', {'class': True}) for idx, item in enumerate(glsas): print('[{0}] {1}'.format(idx, item.find('a').text)) i = self.get_int_input("Which GLSA to release? ") print("Selected '{0}'".format(glsas[i].a.text)) num = glsas[i].a['href'][-4:] prepare_path = '/glsas/{}/prepare_release'.format(num) release_path = '/glsas/{}/release'.format(num) xml_path = '/glsas/{}/download.xml'.format(num) txt_path = '/glsas/{}/download.txt'.format(num) finalize_path = '/glsas/{}/finalize_release'.format(num) data = { 'value': 'Release >', 'authenticity_token': self.get_csrf_token(prepare_path) } # Click the "release" button released_soup = bs(self.request(release_path, method='POST', data=data), features='lxml') glsa_id = 'glsa-' + released_soup.find('strong').text.split()[1] # If there are red flags glsa_id will end up being 'for', so # bail out on the releasing process just like GLSAMaker would assert 'for' not in glsa_id # Grab the xml xml = self.request(xml_path) xml_filename = '{}.xml'.format(glsa_id) xml_path = os.path.join(self.glsa_path, xml_filename) # Write and (TODO) commit it with open(xml_path, 'w') as f: f.write(xml) print("Wrote {}".format(xml_filename)) # Grab the mail text txt = self.request(txt_path) txt_filename = '{}.txt'.format(glsa_id) # Write it with open(txt_filename, 'w') as f: f.write('From: {}\n'.format(self.from_str)) f.write('Reply-To: security@gentoo.org\n') f.write(txt) print("Wrote {}".format(txt_filename)) # Close bugs self.request(finalize_path, 'POST', data={'close_bugs': 1}) # TODO: Mail it def new_whiteboard(self, old_whiteboard): regex = re.compile('[A-C~][0-4] \[.*\]') severity = old_whiteboard[:2] new = ['glsa'] if not regex.match(old_whiteboard): # Don't even try to operate on a whiteboard with a strange # format raise RuntimeError("Bad whiteboard! '" + old_whiteboard + "'") # Iterate over words within the [] part of whiteboard for word in re.sub('[\[\]]', '', old_whiteboard[2:]).split(): if 'glsa' not in word: new += [word] if 'cve' not in new: new.append('cve') return severity + ' [' + ' '.join(new) + ']' def update_bugs(self, bugs): for bug in self.bgo.getbugs(bugs): update = {'whiteboard': self.new_whiteboard(bug.whiteboard), 'comment': {'comment': 'GLSA request filed.'}} print('https://bugs.gentoo.org/{}: {} -> {}' .format(str(bug.id), bug.whiteboard, update['whiteboard'])) self.bgo.update_bugs([bug.id], update) def new_glsa(self, title, bugs): data = { 'title': title + ' [DRAFT]', 'bugs': ','.join(bugs), 'access': 'public', 'import_references': '1', 'what': 'request', # ??? 'authenticity_token': self.get_csrf_token('/glsas/new') } self.request('/glsas', method='POST', data=data) print("GLSA request filed") self.update_bugs(bugs) def glsamaker_key(): authpath = os.path.join(os.path.expanduser('~'), '.config', 'cvetool_auth') if 'CVETOOL_AUTH' in os.environ: return os.environ['CVETOOL_AUTH'] if os.path.isfile(authpath): with open(authpath, 'r') as authfile: return authfile.readlines()[0] def bgo_key(): bugzrc = os.path.expanduser("~/.bugzrc") config = ConfigParser() config.read(bugzrc) apikey = config['default']['key'] return apikey def glsatool(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='command') new_parser = subparsers.add_parser('new') new_parser.add_argument('-b', '--bugs', required=True, nargs='+') new_parser.add_argument('-t', '--title', required=True) release_parser = subparsers.add_parser('release') release_parser.add_argument('-i', '--id') args = parser.parse_args() auth = glsamaker_key() gtool = GLSATool(auth, bgo_key()) if args.command == 'new': for bug in args.bugs: CVETool(auth, 'dobug', [bug]) gtool.new_glsa(args.title, args.bugs) elif args.command == 'release': if args.id: gtool.release_glsa(num=args.id) else: gtool.release_glsa() else: print("No command given!")