Skip to content
Snippets Groups Projects
script.py 17.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/usr/bin/env python3
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    from gvm.connections import TLSConnection
    from gvm.protocols.gmpv208 import Gmp, AliveTest 
    from gvm.transforms import EtreeTransform
    from gvm.xml import pretty_print
    from time import time, sleep
    import logging
    import json
    import base64
    from sys import argv, exit
    
    import os
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    def get_version_old():
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    	with Gmp(connection, transform=transform) as gmp:
    		gmp.authenticate(auth_name, auth_passwd)	
    		pretty_print(gmp.get_version())
    
    
    def create_connection():
        connection_retries = 5
        retry = connection_retries
        while(retry > 0):
            try:
                gmp = Gmp(connection, transform=transform)
                gmp.authenticate(auth_name, auth_passwd)
                return gmp
            except:
    
                logging.warning(f"Connection error with the gmp endpoint. Remaining {retry} retries")
    
        raise Exception("Impossible connect to the gmp endpoint even after 5 retries")
    
    
    def get_version():
        gmp = create_connection()
        res = gmp.get_version()
    
        return res.xpath('version/text()')[0]
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    ########## PORT LIST ##################################
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    def create_port_list(port_list_name, ports):
    
        gmp = create_connection()
        res = gmp.create_port_list(port_list_name, ','.join(ports))
        status = res.xpath('@status')[0]
        status_text = res.xpath('@status_text')[0]
        if status == "201":
            id = res.xpath('@id')[0]
    
    Gioacchino Vino's avatar
    Gioacchino Vino committed
            logging.debug(f'Created port list obj. Name: {port_list_name}, id: {id}, ports: {ports}')
    
            return {'name': port_list_name, 'id': id}
        else:
    
            logging.error(f"ERROR during Port list creation. Status code: {status}, msg: {status_text}")
    
            msg = f"ERROR during Port list creation. Status code: {status}, msg: {status_text}"
            raise Exception(msg) 
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    def get_port_lists(filter_str="rows=-1"):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        l_o = []
    
        gmp = create_connection()
        res = gmp.get_port_lists(filter_string=filter_str)
        for pl in res.xpath('port_list'):
            o = dict()
            o['name'] = pl.xpath('name/text()')[0]
            o['id'] = pl.xpath('@id')[0]
            o['in_use'] = pl.xpath('in_use/text()')[0]
            l_o.append(o)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        return l_o
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    def delete_port_list(port_list):
    
        gmp = create_connection()
    
        res = gmp.delete_port_list(port_list['id'])
    
        status = res.xpath('@status')[0]
        status_text = res.xpath('@status_text')[0]
        if status == "200":
    
            logging.info(f"Port_list with id: {port_list['id']} and name: {port_list['name']} DELETED") 
    
            logging.error(f"ERROR {status}: {status_text}")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def get_or_create_port_list(port_list_name, ports):
        res = get_port_lists(port_list_name)
        if len(res) == 0:
            port_list = create_port_list(port_list_name, ports)
            return get_port_lists(port_list['id'])[0]
        elif len(res) == 1:
            return res[0]
        else:
    
            logging.warning(f"Found {len(res)} results.")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
            return res
    
    ############## TARGET  ##################################
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def create_target(name,ip,port_list,ovs_ssh_credential):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        o = dict()
    
        gmp = create_connection()
        res = gmp.create_target(
                name=name,
                comment = "",
                hosts=[ip],
                port_list_id = port_list['id'],
                ssh_credential_id = ovs_ssh_credential['id'],
                alive_test=AliveTest('Consider Alive'))
        status = res.xpath('@status')[0]
        status_text = res.xpath('@status_text')[0]
        if status == "201":
            id = res.xpath('@id')[0]
            return {'name': name, 'id': id}
        else:
            msg = f"ERROR during Target creation. Status code: {status}, msg: {status_text}"
            raise Exception(msg) 
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    def get_targets(filter_str):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        res = []
    
        gmp = create_connection()
        targets = gmp.get_targets(filter_string=filter_str)
        for target in targets.xpath('target'):
            o = dict()
            o['name'] = target.xpath('name/text()')[0]
            o['hosts'] = target.xpath('hosts/text()')[0]
            o['id'] = target.xpath('@id')[0]
            o['in_use'] = target.xpath('in_use/text()')[0]
            res.append(o)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        return res
    
    def delete_target(target):
    
        gmp = create_connection()
        res = gmp.delete_target(target['id'])
        status = res.xpath('@status')[0]
        status_text = res.xpath('@status_text')[0]
        if status == "200":
    
            logging.info(f"Port_list with id: {target['id']} and name: {target['name']} DELETED") 
    
            logging.error(f"ERROR {status}: {status_text}")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def get_or_create_target(target_name,ip,port_list,ovs_ssh_credential):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        res = get_targets(target_name)
        if len(res) == 0:
    
    qweqweasdasd's avatar
    qweqweasdasd committed
            t = create_target(target_name,ip,port_list,ovs_ssh_credential)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
            return get_targets(t['id'])[0]
        elif len(res) == 1:
            return res[0]
        else:
            print(f"Found {len(res)} results. Return None")
            return res
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    def search_and_delete_target(target_name):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        targets = get_targets(target_name)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        if len(targets) == 1:
            delete_target(targets[0]['id'])
        else:
            raise("Multiple results for search")
    
    def search_and_delete_all_targets(target_name):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        targets = get_targets(target_name)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        for target in targets:
            delete_target(target)
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    ############## TASK ##################################
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def create_task(name, config, target, scanner):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        o = dict()
    
        gmp = create_connection()
        res = gmp.create_task(
                name=name,
                config_id=config['id'],
                target_id=target['id'],
                scanner_id=scanner['id'])
        status = res.xpath('@status')[0]
        status_text = res.xpath('@status_text')[0]
        if status == "201":
            id = res.xpath('@id')[0]
            return {'name': name, 'id': id}
        else:
            msg = f"ERROR during Task creation. Status code: {status}, msg: {status_text}"
            raise Exception(msg)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def get_tasks(filter_str):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        res = []
    
        gmp = create_connection()
        tasks = gmp.get_tasks(filter_string=filter_str)
        for task in tasks.xpath('task'):
                o = dict()
                o['name'] = task.xpath('name/text()')[0]
                o['id'] = task.xpath('@id')[0]
                o['progress'] = task.xpath('progress/text()')[0]
                o['in_use'] = task.xpath('in_use/text()')[0]
                o['status'] = task.xpath('status/text()')[0]
                o['target_id'] = task.xpath('target/@id')[0]
                try:
                    o['report_id'] = task.xpath('last_report/report/@id')[0]
                except:
                    pass
                res.append(o)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        return res
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def get_or_create_task(task_name, config, target, scanner):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        res = get_tasks(task_name)
        if len(res) == 0:
    
    qweqweasdasd's avatar
    qweqweasdasd committed
            t = create_task(task_name, config, target, scanner)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
            return get_tasks(t['id'])[0]
        elif len(res) == 1:
            return res[0]
        else:
            print(f"Found {len(res)} results. Return None")
            return res
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def get_all_tasks():
        res = []
    
        gmp = create_connection()
        tasks = gmp.get_tasks(filter_string="rows=-1")
        for task in tasks.xpath('task'):
                o = dict()
                o['name'] = task.xpath('name/text()')[0]
                o['id'] = task.xpath('@id')[0]
                o['progress'] = task.xpath('progress/text()')[0]
                o['in_use'] = task.xpath('in_use/text()')[0]
                o['status'] = task.xpath('status/text()')[0]
                o['target_id'] = task.xpath('target/@id')[0]
                try:
                    o['report_id'] = task.xpath('last_report/report/@id')[0]
                except:
                    pass
                res.append(o)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        return res
    
    def search_and_delete_all_tasks(filter_str):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        tasks = get_tasks(filter_str)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        for task in tasks:
            delete_task(task)
    
    def start_task(task):
    
        gmp = create_connection()
        res = gmp.start_task(task['id'])
        task['report_id'] = res.xpath('report_id/text()')[0]
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        return task        
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    def stop_task(task):
    
        gmp = create_connection()
        res = gmp.stop_task(task['id'])
        pretty_print(res)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    def delete_task(task):
    
        gmp = create_connection()
        res = gmp.delete_task(task['id'])
        status = res.xpath('@status')[0]
        status_text = res.xpath('@status_text')[0]
        if status == "200":
    
            logging.info(f"Target with id: {task['id']} and name: {task['name']} DELETED") 
    
            logging.error(f"ERROR {status}: {status_text}")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    ############## REPORTS #####################################3
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    class report_formats:
        anonymous_xml = "5057e5cc-b825-11e4-9d0e-28d24461215b"
        csv_results   = "c1645568-627a-11e3-a660-406186ea4fc5"
        itg           = "77bd6c4a-1f62-11e1-abf0-406186ea4fc5"
        pdf           = "c402cc3e-b531-11e1-9163-406186ea4fc5"
        txt           = "a3810a62-1f62-11e1-9219-406186ea4fc5"
        xml           = "a994b278-1f62-11e1-96ac-406186ea4fc5"
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def get_report_formats():
    
        gmp = create_connection()  
        res =  gmp.get_report_formats()
        for f in res.xpath('report_format'):
            name = f.xpath('name/text()')[0]
            id = f.xpath('@id')[0]
            print(id,name)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    def get_report_format(id):
    
        gmp = create_connection()
        res =  gmp.get_report_formats()
        pretty_print(res)  
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def get_progress(task):
        task_info = get_tasks(task['id'])[0]
        status = task_info['status']         #   New -> Requested -> Queued -> Running  -> Done
        progress = int(task_info['progress'])#    0         0           0      0 -> 100     -1
        return status, progress
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    def wait_for_task_ending(task, timeout=3600):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        start_time = time()
    
        logging.info("Waiting for scans ends the task")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        while True:
    
    qweqweasdasd's avatar
    qweqweasdasd committed
            status, progress = get_progress(task)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
            if status not in ["New","Requested","Queued","Running","Done"]: # ["Interrupted", ...]
    
                logging.warning(f"Waiting for scans ends the task. Status: {status}")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
                return False
    
    qweqweasdasd's avatar
    qweqweasdasd committed
            if status == "Done" and progress == -1:
    
                logging.info(f"Waiting for scans ends the task. Status: {status}")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
                return True
            if time() - start_time > timeout:
    
                    logging.error("TIMEOUT during waiting for task ending")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
                    return False
    
            logging.debug(f"Waiting for the task ends. Now {int(time() - start_time)}s from start. Status: {status}")
            sleep(10)
        
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def save_report(task,report_format_id, report_filename ):
    
        gmp = create_connection()
        res = gmp.get_report(task['report_id'],
                                    report_format_id=report_format_id, 
                                    ignore_pagination=True,
                                    details="1")
        code = str(res.xpath('report/text()')[0])
        with open(report_filename, "wb") as fh:
            fh.write(base64.b64decode(code))
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def save_severity_report(task, severity_filename):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        dict_severity = {"Log": 0, "Low": 1, "Medium": 2, "High": 3}
    
        gmp = create_connection()
        res = gmp.get_report(task['report_id'],
                            report_format_id=report_formats.anonymous_xml, 
                            ignore_pagination=True,
                            details="1")
        severities = res.xpath('report/report/ports/port/threat/text()')
        old_num_severity = 0
        severity = "Log"
        for sev in severities:
            if dict_severity[sev] > old_num_severity:
                old_num_severity = dict_severity[sev]
                severity = sev
        with open(severity_filename, "w") as f:
            f.write(severity)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    def get_report_info(task):
        report = dict()
    
        gmp = create_connection()
        res = gmp.get_report(task['report_id'],
    
                            report_format_id=report_formats.anonymous_xml,
    
                            ignore_pagination=True,
                            details="1")
    
        threats = res.xpath('report/report/ports/port/threat/text()')
        ports = res.xpath('report/report/ports/port/text()')
        severities = res.xpath('report/report/ports/port/severity/text()')
        severities = list(map(lambda a : float(a), severities))
        for p,t,s in zip(ports, threats, severities):
            report[p] = {'severity': s, 'threat': t}
        glob_severity = -1 # returned severities are null or positive
        glob_threat = 'Log'
        for threat,severity in zip(threats,severities):
            if severity > glob_severity:
                glob_severity = severity
                glob_threat = threat
                glob_severity = severity
    
        report['global'] = {'threat': glob_threat, 'severity': glob_severity}
        return report
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def get_reports(filter_str="rows=-1"):
        lo = []
    
        gmp = create_connection()
        reports = gmp.get_reports(filter_string = filter_str)
        for report in reports.xpath('report'):
            o = dict()
            o['task_name'] = report.xpath('task/name/text()')[0]
            o['id'] = report.xpath('@id')[0]
            lo.append(o)
        return lo
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    def get_numeric_severity(severity):
        if severity == "Log":
            return 0
        elif severity == "Low":
            return 1
        elif severity == "Medium":
            return 2
        elif severity == "High":
            return 3
        else:
            return 4
    
    def get_severity_from_number(num):
        if num == 0:
            return "Low"
        elif num == 1:
            return "Low"
        elif num == 2:
            return "Medium"
        elif num == 3:
            return "High"
        else:
            return "Undefined"
    
    
    def process_global_reports_info(reports):
        glob_severity = -1
        glob_threat = 'Log'
        for host in reports:
            host_glob_severity = reports[host]['global']['severity']
            if host_glob_severity > glob_severity:
                glob_severity = host_glob_severity
                glob_threat = reports[host]['global']['threat']
        reports['deployment'] = {'severity': glob_severity, 
                                 'threat': glob_threat}
    
        if reports['deployment']['severity'] < 4:
    
            reports['global'] = "OK"
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        else:
    
            reports['global'] = "NOK"
        return reports
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    Gioacchino Vino's avatar
    Gioacchino Vino committed
    def pretty_json(j):
        return json.dumps(j,sort_keys=True,indent=4)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
            
    def import_dep_info(file_path, endpoints_to_scan):
        with open(file_path) as f:
    
            data = json.load(f)    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        endpoints = dict()
        for key in data['outputs'].keys():
            if key in endpoints_to_scan:
    
                endpoint = str(data['outputs'][key])
                prefix,url = endpoint.split("://")
                if ":" in url:
                    host,port = url.split(":")
                else:
                    host = url
                    if prefix == "https":
    
                    elif prefix == 'http':
    
                    else:
                        raise Exception(f"Impossible to parse the endpoint port. Endpoint: {endpoint}")
    
                logging.info(f"Endpoint: {host}:{port}")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
                if host not in endpoints:
                    endpoints[host] = {"22"}
                endpoints[host].add(port)
    
        for host,ports in endpoints.items():
            endpoints[host] = sorted(list(ports))
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        return endpoints
                    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    ################ MAIN #######################################
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    logging.basicConfig(
    
    Gioacchino Vino's avatar
    Gioacchino Vino committed
        filename='scans.log', 
    
        level=logging.DEBUG,
        format='%(asctime)s %(levelname)-8s %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S',
        filemode='w')
    logging.info("\n\nStart scan application")
    
    if os.environ.get('GMP_USER') is not None and \
            os.environ.get('GMP_USER') != '':
        auth_name = os.getenv('GMP_USER')
    else:
        logging.error("GMP_USER env var is not defined\nexit")
        raise Exception("GMP_USER env var is not defined")
    
    if os.environ.get('GMP_PASSWORD') is not None and \
            os.environ.get('GMP_PASSWORD') != '':
        auth_passwd = os.getenv('GMP_PASSWORD')
    else:
        logging.error("GMP_PASSWORD env var is not defined\nexit")
        raise Exception("GMP_PASSWORD env var is not defined")
    
    
    local_ip = "127.0.0.1"
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    connection = TLSConnection(hostname=local_ip)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    transform = EtreeTransform()
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    config = {'id':"9866edc1-8869-4e80-acac-d15d5647b4d9"}
    scanner = {'id': "08b69003-5fc2-4037-a479-93b440211c73"}
    
    ovs_ssh_credential = {'id': "b9af5845-8b87-4378-bca4-cee39a894c17"}
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    wait_timeout = 3600 #1h
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    if len(argv) != 4:
        print("Please pass three parameters:")
        print("- endpoints to scans [endpoints1,endpoint2,endpoint3,...]")
        print("- dep.json path [/home/gmp/workspace/dep.json]")
        print("- output directory [/home/gmp/workspace]")
        exit(1)
    
    
    Gioacchino Vino's avatar
    Gioacchino Vino committed
    logging.info(f'Configured Scans endpoint: {local_ip}')
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    endpoints_to_scan = argv[1].split(',')
    
    dep_json = argv[2]
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    output_dir = argv[3]
    
    Gioacchino Vino's avatar
    Gioacchino Vino committed
    logging.info(f"endpoints_to_scan: {endpoints_to_scan}")
    logging.info(f"dep_json: {dep_json}")
    logging.info(f"output_dir: {output_dir}")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    endpoints = import_dep_info(dep_json, endpoints_to_scan)
    
    logging.info(f"endpoints: {endpoints}")
    
    
    # test gmp connection
    
    Gioacchino Vino's avatar
    Gioacchino Vino committed
    logging.info(f"gvm version: {get_version()}")
    
    reports = dict()
    
    qweqweasdasd's avatar
    qweqweasdasd committed
    for host,ports in endpoints.items():
    
        logging.info(f"endpoint: {host}:{ports}")
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        target_name = f"{auth_name}_target_{host}"
        task_name = f"{auth_name}_task_{host}"
        port_list_name = f"{auth_name}_pl_{host}"
    
        report_filename = f"{output_dir}/{host}-report.txt"
    
        summary_filename = f"{output_dir}/summary-report.json"
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        port_list = get_or_create_port_list(port_list_name,ports)
    
    Gioacchino Vino's avatar
    Gioacchino Vino committed
        logging.info(f"Port list:\n {pretty_json(port_list)}")
        
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        target = get_or_create_target(target_name,host,port_list,ovs_ssh_credential)
    
    Gioacchino Vino's avatar
    Gioacchino Vino committed
        logging.info(f"Target:\n {pretty_json(target)}")
        
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        task = get_or_create_task(task_name, config, target,scanner)
    
    Gioacchino Vino's avatar
    Gioacchino Vino committed
        logging.info(f"Task:\n {pretty_json(task)}")
        
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        if task['status'] == 'New':
            task = start_task(task)
    
        if wait_for_task_ending(task,wait_timeout):
    
    qweqweasdasd's avatar
    qweqweasdasd committed
            save_report(task,report_formats.txt, report_filename)
    
            reports[host] = get_report_info(task)
    
    qweqweasdasd's avatar
    qweqweasdasd committed
        else:
    
            reports[host] = f"ERROR Task: {task['id']}"
    
    Gioacchino Vino's avatar
    Gioacchino Vino committed
        
    
        delete_task(task)
        delete_target(target)
        delete_port_list(port_list)
    
    
    reports = process_global_reports_info(reports)
    
    
    logging.info(pretty_json(reports))
    
    
    with open(summary_filename, "w") as f:
    
        f.write(json.dumps(reports))