예제 #1
0
def show_attachments(parsed_eml: Message):
    print_headline_banner('Attachments')
    attachments = list()
    for child in parsed_eml.walk():
        if child.get_filename() is not None:
            attachment_filename = _get_printable_attachment_filename(
                attachment=child)
            attachments.append(
                (attachment_filename, str(child.get_content_type()),
                 str(child.get_content_disposition())))
    if len(attachments) == 0:
        info('E-Mail contains no attachments')
    else:
        max_width_filename = max([
            len(filename)
            for (filename, content_type, disposition) in attachments
        ]) + 7
        max_width_content_type = max([
            len(content_type)
            for (filename, content_type, disposition) in attachments
        ]) + 7
        for index, (filename, content_type,
                    disposition) in enumerate(attachments):
            index_str = '[' + colorize_string(text=str(index + 1),
                                              color=Color.CYAN) + ']'
            print(index_str, filename.ljust(max_width_filename),
                  content_type.ljust(max_width_content_type), disposition)
    print()
예제 #2
0
def extract_attachment(parsed_eml: Message, attachment_number: int,
                       output_path: str or None):
    print_headline_banner('Attachment Extracting')
    attachment = None
    counter = 1
    for child in parsed_eml.walk():
        if child.get_filename() is not None:
            if counter == attachment_number:
                attachment = child
                break
            counter += 1

    # Check if attachment was found
    if attachment is None:
        error('Attachment {} could not be found'.format(attachment_number))
        return

    attachment_filename = _get_printable_attachment_filename(
        attachment=attachment)

    info('Found attachment [{}] "{}"'.format(attachment_number,
                                             attachment_filename))

    if output_path is None:
        output_path = attachment.get_filename()
    elif os.path.isdir(output_path):
        output_path = os.path.join(output_path, attachment_filename)

    payload = attachment.get_payload(decode=True)
    output_file = open(output_path, mode='wb')
    output_file.write(payload)
    info('Attachment extracted to {}'.format(output_path))
예제 #3
0
def extract_all_attachments(parsed_eml: Message, path: str or None):
    print_headline_banner('Attachment Extracting')

    # if no output directory is given then a default directory with the name 'eml_attachments' is used
    if path is None:
        path = 'eml_attachments'

    if not os.path.exists(path):
        os.makedirs(path)

    counter = 0
    for child in parsed_eml.walk():
        if child.get_filename() is None:
            continue
        counter += 1

        attachment_filename = _get_printable_attachment_filename(
            attachment=child)

        output_path = os.path.join(path, attachment_filename)

        # write attachment to disk
        payload = child.get_payload(decode=True)
        output_file = open(output_path, mode='wb')
        output_file.write(payload)

        info('Attachment [{}] "{}" extracted to {}'.format(
            counter, attachment_filename, output_path))
예제 #4
0
def show_html(parsed_eml: Message):
    print_headline_banner(headline='HTML')
    html = __get_decoded_payload(parsed_eml=parsed_eml, content_type='text/html')
    if html is None:
        info('Email contains no HTML')
    else:
        print(html)
    print()
예제 #5
0
def show_text(parsed_eml: Message):
    print_headline_banner(headline='Plaintext')
    text = __get_decoded_payload(parsed_eml=parsed_eml, content_type='text/plain')
    if text is None:
        info('Email contains no plaintext')
    else:
        print(text)
    print()
예제 #6
0
def get_all_emails_of_a_user(username: str,
                             repo_name: str) -> Dict[str, Set[str]]:
    emails_to_name = defaultdict(set)
    seen_commits = set()
    page_counter = 1
    commit_counter = 1

    while True:
        continue_loop = True
        url = '{}/repos/{}/{}/commits?per_page=100&page={}'.format(
            API_URL, username, repo_name, page_counter)
        result_dict = __api_call(url=url)

        if 'message' in result_dict:
            if result_dict['message'] == 'Git Repository is empty.':
                info('Git repository is empty', verbosity_level=5)
                continue

            if 'API rate limit exceeded for ' in result_dict['message']:
                warning(
                    'API rate limit exceeded - not all repos where fetched')
                return emails_to_name

            if result_dict['message'] == 'Not Found':
                warning('There is no repository with the name "{}"'.format(
                    repo_name))
                return emails_to_name

        for commit_dict in result_dict:
            sha = commit_dict['sha']
            if sha in seen_commits:
                continue_loop = False
                break

            seen_commits.add(sha)
            info('scan commit {}'.format(commit_counter), verbosity_level=5)
            commit_counter += 1

            if commit_dict['author'] is None:
                continue
            user = commit_dict['author']['login']
            if user.lower() == username.lower():
                commit = commit_dict['commit']
                author_name = commit['author']['name']
                author_email = commit['author']['email']
                committer_name = commit['committer']['name']
                committer_email = commit['committer']['email']
                emails_to_name[author_email].add(author_name)
                emails_to_name[committer_email].add(committer_name)
        if continue_loop and len(result_dict) == 100:
            page_counter += 1
        else:
            break
    return emails_to_name
예제 #7
0
def show_urls(parsed_eml: Message):
    print_headline_banner(headline='URLs in HTML part')
    all_links = set()
    html_str = __get_decoded_payload(parsed_eml=parsed_eml, content_type='text/html')
    if html_str is None:
        warning('Email contains no HTML')
    else:
        for pattern in [r'href="(.+?)"', r"href='(.+?)'"]:
            for match in re.finditer(pattern, html_str):
                all_links.add(match.group(1))
        if len(all_links) == 0:
            info(message='No URLs found in the html')
        for x in all_links:
            print(' - ' + colorize_string(text=x, color=Color.MAGENTA))
    print()
예제 #8
0
def check_tracking(parsed_eml: Message):
    print_headline_banner(headline='Reloaded Content (aka. Tracking Pixels)')
    sources = set()
    html_str = __get_decoded_payload(parsed_eml=parsed_eml, content_type='text/html')
    if html_str is None:
        warning('Email contains no HTML')
    else:
        for pattern in [r'src="(.+?)"', r"src='(.+?)'", r'background="(.+?)"', r"background='(.+?)'"]:
            for match in re.finditer(pattern, html_str):
                if not match.group(1).startswith('cid:'):
                    sources.add(match.group(1))
        if len(sources) == 0:
            info(message='No content found which will be reloaded from external resources')
        for x in sources:
            print(' - ' + colorize_string(text=x, color=Color.MAGENTA))
    print()
예제 #9
0
def __final_mount_procedure(source_path: str, target_path: str,
                            partition_number: int):
    if not os.path.exists(target_path):
        os.makedirs(target_path)

    mount_options = list()

    while True:
        user_input = input('Mount in readonly mode (y/n) [y]: ')
        if len(user_input) == 0 or user_input == 'y':
            mount_options.append('ro')
            break
        elif user_input == 'n':
            break
        else:
            error(message='input not valid.')
            print()

    mount_options.append('show_sys_files')

    while True:
        user_input = input('Mount as NTFS filesystem (y/n) [y]: ')
        if len(user_input) == 0 or user_input == 'y':
            mount_options.append('streams_interface=windows')
            break
        elif user_input == 'n':
            break
        else:
            error(message='input not valid.')
            print()

    # generate mount command
    mount_options_string = ','.join(mount_options)

    mount_command = 'mount -o {} "{}" "{}" >/dev/null 2>&1'.format(
        mount_options_string, __escape_path(source_path),
        __escape_path(target_path))

    if int(os.system(mount_command)) == 0:
        info(message='Partition {} was mounted under "{}"'.format(
            partition_number, __escape_path(target_path)))
    else:
        error('An Error occurred. Partition could not be mounted')
예제 #10
0
 def __run(self):
     while True:
         try:
             filename, file_extension, content = self.queue.get(block=True,
                                                                timeout=0.5)
         except Empty:
             if not self.is_running:
                 break
         else:
             # store the carved file on the drive
             output_directory_for_file_type = os.path.join(
                 self.output_directory, file_extension)
             if not os.path.exists(output_directory_for_file_type):
                 os.makedirs(output_directory_for_file_type)
             path_to_file = os.path.join(
                 output_directory_for_file_type,
                 '{}.{}'.format(filename, file_extension))
             with open(path_to_file, mode='wb') as carved_file:
                 carved_file.write(content)
             info('Carved new file: {}'.format(path_to_file))
예제 #11
0
    def __scan_data(self):
        info('start file carving...')
        sector_before = bytes()
        self.scanned_sectors = 0
        self.worker_list = list()
        while True:
            try:
                next_sector = self.buffer.get(block=True, timeout=0.01)

                # update current worker
                workers_to_remove = list()
                for worker in self.worker_list:
                    maintain_worker = worker.update(next_sector)
                    if not maintain_worker:
                        workers_to_remove.append(worker)

                # remove workers which which should no longer be maintained
                for worker in workers_to_remove:
                    self.worker_list.remove(worker)

                # check for new start triggers
                search_bytes = sector_before + next_sector
                index_offset = max(
                    (self.scanned_sectors - 1), 0) * self.size_of_a_sector
                for plugin in self.plugin_list:
                    new_workers = plugin.check_for_trigger_sequence(
                        index_offset=index_offset, search_data=search_bytes)
                    self.worker_list.extend(new_workers)

                sector_before = next_sector
                self.scanned_sectors += 1

            except Empty:
                if self.scanned_sectors >= self.number_of_sectors != 0:
                    for worker in self.worker_list:
                        worker.end_of_data_signal()
                    info('Complete Input was processed')
                    break
예제 #12
0
def crack_zip_archive(password_list: list,
                      zip_archive: zipfile.ZipFile) -> str or None:
    """ returns password of the zip file or None if the password was not in the password list """
    if not __archive_is_encrypted(zip_archive=zip_archive):
        warning('Zip file is not encrypted')
    info('Try {} passwords'.format(len(password_list)))
    for x in password_list:
        try:
            zip_archive.extractall(pwd=x.encode('utf-8'))
            info('Password found: "{}"'.format(x))
            info('Files extracted')
            return x
        except KeyboardInterrupt:
            warning('Keyboard Interruption. Existing.')
        except:
            pass
    info('Password not found')
    return None
예제 #13
0
    def __read_data(self, device):
        self.number_of_sectors = device.length
        self.size_of_a_sector = device.sectorSize
        info('Model of input device: {}'.format(device.model))
        info('Number of sectors: {}'.format(self.number_of_sectors))
        info('Bytes per sector: {}'.format(self.size_of_a_sector))

        try:
            with open(self.path_to_input_device, mode='rb') as input_file:
                input_file.seek(0)
                for sector_i in range(self.number_of_sectors + 1):
                    data_of_new_sector = input_file.read(self.size_of_a_sector)
                    self.buffer.put(data_of_new_sector, block=True)
        except IsADirectoryError:
            error('The Input has to be a drive not an directory')
            self.is_running = False
            exit()
예제 #14
0
def main():
    parser = argparse.ArgumentParser(usage='run.py -i INPUT -o OUTPUT')
    parser.add_argument(
        '-i',
        '--input',
        type=str,
        help='Path to the device or file which should be scanned')
    parser.add_argument('-o',
                        '--output',
                        type=str,
                        help='Path to the output directory')
    parser.add_argument(
        '-c',
        '--no-corruption-checks',
        dest='no_corruption_checks',
        help='No corruption checks will be made, which faster the scan',
        action='store_true',
        default=False)
    parser.add_argument(
        '-f',
        dest='flush_if_maximum_file_size_is_reached',
        help=
        'Flush files when the maximum file size is reached even if its not completely carved',
        action='store_true',
        default=False)
    parser.add_argument(
        '-p',
        '--plugin',
        type=str,
        nargs='+',
        dest='plugin_list',
        help=
        'List of plugins which will be used [keys, cert, pictures, binary, pdf]',
        default=['keys', 'cert', 'pictures', 'binary', 'pdf'])

    arguments = parser.parse_args()

    if arguments.input is None:
        parser.print_help()
        exit()
    arguments.input = os.path.abspath(arguments.input)

    if arguments.output is None:
        parser.print_help()
        exit()
    arguments.output = os.path.abspath(arguments.output)

    file_carver = FileCarver(
        path_to_input_device=arguments.input,
        output_directory=arguments.output,
        make_corruption_checks=not arguments.no_corruption_checks,
        flush_if_maximum_file_size_is_reached=arguments.
        flush_if_maximum_file_size_is_reached)

    # Private Keys
    if 'keys' in arguments.plugin_list:
        file_carver.register_plugin(EncryptedPrivateKey)
        file_carver.register_plugin(PrivateKey)
        file_carver.register_plugin(EncryptedPrivateKey)
        file_carver.register_plugin(PrivateDSAKey)
        file_carver.register_plugin(PrivateECKey)
        file_carver.register_plugin(PrivateRsaKey)

    # Certificates and Certificate Requests
    if 'cert' in arguments.plugin_list:
        file_carver.register_plugin(CertificateRequest)
        file_carver.register_plugin(Certificate)
        file_carver.register_plugin(TrustedCertificate)

    # Pictures
    if 'pictures' in arguments.plugin_list:
        file_carver.register_plugin(JPG)
        file_carver.register_plugin(PNG)

    # Binaries
    if 'binary' in arguments.plugin_list:
        file_carver.register_plugin(PeFile)

    # PDF
    if 'pdf' in arguments.plugin_list:
        file_carver.register_plugin(PDF)

    file_carver.start_scanning()

    try:
        time_started = time.time()
        info('Starting file carving process...')
        while file_carver.is_running:
            scanned_sectors = file_carver.scanned_sectors
            if scanned_sectors > 0:
                number_of_sectors = file_carver.number_of_sectors
                progress_in_percent = 100 * (scanned_sectors /
                                             number_of_sectors)
                # predict time it still takes
                duration_up_to_now = time.time() - time_started
                prediction_in_sec = (duration_up_to_now / scanned_sectors) * (
                    number_of_sectors - scanned_sectors)
                d_hours, d_minutes, d_seconds = __convert_seconds(
                    seconds_total=duration_up_to_now)
                p_hours, p_minutes, p_seconds = __convert_seconds(
                    seconds_total=prediction_in_sec)
                info('{:2.2f}%    duration: {}:{}:{}  -  remaining: {}:{}:{}'.
                     format(progress_in_percent, d_hours, d_minutes, d_seconds,
                            p_hours, p_minutes, p_seconds))
            time.sleep(1)

        d_hours, d_minutes, d_seconds = __convert_seconds(time.time() -
                                                          time_started)
        info('File carving process is complete. Needed: {}:{}:{}'.format(
            d_hours, d_minutes, d_seconds))

    except KeyboardInterrupt:
        warning('Keyboard Interrupt! Existing.')
        exit()
예제 #15
0
def program(input_path: str, mounting_path: str):
    # create directory which contains all mounting endpoints
    if not os.path.exists(mounting_path):
        try:
            os.makedirs(mounting_path)
        except PermissionError:
            error('Permission denied for creating directory "{}"'.format(
                mounting_path))
            exit()

    # mount the ewf file
    mounting_path_ewf_dir = os.path.join(mounting_path, '.ewf')
    if not os.path.exists(mounting_path_ewf_dir):
        try:
            os.makedirs(mounting_path_ewf_dir, exist_ok=True)
        except PermissionError:
            error('Permission denied for creating directory "{}"'.format(
                mounting_path_ewf_dir))
            exit()

    if int(
            os.system('ewfmount "{}" "{}" >/dev/null 2>&1'.format(
                __escape_path(input_path),
                __escape_path(mounting_path_ewf_dir)))) != 0:
        error(message=
              'An error occurred while mounting ewf file to "{}". Exiting.'.
              format(mounting_path_ewf_dir))
        exit()
    else:
        info(message='ewf file mounted to "{}"'.format(mounting_path_ewf_dir))

    # get the path to the ewf file
    path_to_the_mounted_ewf_file = None
    for file_path in os.listdir(mounting_path_ewf_dir):
        path_to_the_mounted_ewf_file = os.path.join(mounting_path_ewf_dir,
                                                    file_path)
    if path_to_the_mounted_ewf_file is None:
        info(message='Could not find mounted ewf file. Exiting.')
        exit()

    # Mount ewf tile to unused loop device
    path_to_loop_device = __get_first_unused_loop_device()
    if int(
            os.system('losetup -Pv "{}" "{}" >/dev/null 2>&1'.format(
                __escape_path(path_to_loop_device),
                __escape_path(path_to_the_mounted_ewf_file)))) != 0:
        info(
            message=
            'An error occurred while mounting ewf file to loop back device. Exiting.'
        )
        exit()

    while True:
        info(message='Select Partition to mount:')
        os.system('fdisk -l "{}"'.format(__escape_path(path_to_loop_device)))
        print()
        selected_partition_number = input(
            'select number of partition (0 for complete disk) [1] > ')

        if len(selected_partition_number) == 0:  # Default value
            selected_partition_number = 1
        else:
            # check if partition number is an integer
            try:
                selected_partition_number = int(selected_partition_number)
            except ValueError:
                error('The partition number must be an integer')
                print()
                continue

        if selected_partition_number == 0:
            selected_partition_path = path_to_loop_device
            info(message='selected the complete disk "{}"'.format(
                __escape_path(selected_partition_path)))
        else:
            selected_partition_path = '{}p{}'.format(
                path_to_loop_device, selected_partition_number)
            info(message='selected partition "{}"'.format(
                __escape_path(selected_partition_path)))

        bitlocker_key = input(
            'Bitlocker Recovery Key (if encrypted otherwise empty) > ')

        if len(bitlocker_key) > 0:

            # check if provided key is valid
            if re.fullmatch(r'((\d){6}-){7}(\d{6})', bitlocker_key) is None:
                error(
                    'The format of the recovery key you typed in is invalid.')
                info(
                    'The key must be in the format: DDDDDD-DDDDDD-DDDDDD-DDDDDD-DDDDDD-DDDDDD-DDDDDD'
                )
                print()
                continue

            mounting_path_dislocker = os.path.join(
                mounting_path,
                '.partition_{}_encrypted'.format(selected_partition_number))
            if not os.path.exists(mounting_path_dislocker):
                os.makedirs(mounting_path_dislocker)
            if int(
                    os.system(
                        'dislocker -v -V "{}" -p{} "{}" >/dev/null 2>&1'.
                        format(__escape_path(selected_partition_path),
                               bitlocker_key,
                               __escape_path(mounting_path_dislocker)))) != 0:
                info(message=
                     'An Error occurred. Partition could not be decrypted')
            else:
                mounting_path_dislocker_file = os.path.join(
                    mounting_path_dislocker, 'dislocker-file')
                mounting_path_decrypted = os.path.join(
                    mounting_path,
                    'partition_{}_decrypted'.format(selected_partition_number))
                __final_mount_procedure(
                    source_path=mounting_path_dislocker_file,
                    target_path=mounting_path_decrypted,
                    partition_number=selected_partition_number)
        else:
            mounting_path_partition = os.path.join(
                mounting_path,
                'partition_{}'.format(selected_partition_number))
            __final_mount_procedure(source_path=selected_partition_path,
                                    target_path=mounting_path_partition,
                                    partition_number=selected_partition_number)
        print()
        input('Press ENTER to mount another partition')
예제 #16
0
def main():
    argument_parser = argparse.ArgumentParser(
        usage='emlAnalyzer [OPTION]... -i FILE',
        description=
        'A cli script to analyze an E-Mail in the eml format for viewing the header, extracting attachments etc.'
    )
    argument_parser.add_argument('-i',
                                 '--input',
                                 help="path to the eml-file (is required)",
                                 type=str)
    argument_parser.add_argument('--header',
                                 action='store_true',
                                 default=False,
                                 help="Shows the headers")
    argument_parser.add_argument(
        '-x',
        '--tracking',
        action='store_true',
        default=False,
        help=
        "Shows content which is reloaded from external resources in the HTML part"
    )
    argument_parser.add_argument('-a',
                                 '--attachments',
                                 action='store_true',
                                 default=False,
                                 help="Lists attachments")
    argument_parser.add_argument('--text',
                                 action='store_true',
                                 default=False,
                                 help="Shows plaintext")
    argument_parser.add_argument('--html',
                                 action='store_true',
                                 default=False,
                                 help="Shows HTML")
    argument_parser.add_argument('-s',
                                 '--structure',
                                 action='store_true',
                                 default=False,
                                 help="Shows structure of the E-Mail")
    argument_parser.add_argument(
        '-u',
        '--url',
        action='store_true',
        default=False,
        help="Shows embedded links and urls in the html part")
    argument_parser.add_argument('-ea',
                                 '--extract',
                                 type=int,
                                 default=None,
                                 help="Extracts the x-th attachment")
    argument_parser.add_argument('--extract-all',
                                 action='store_true',
                                 default=None,
                                 help="Extracts all attachments")
    argument_parser.add_argument(
        '-o',
        '--output',
        type=str,
        default=None,
        help=
        "Path for the extracted attachment (default is filename in working directory)"
    )
    arguments = argument_parser.parse_args()

    if arguments.input is None or len(arguments.input) == 0:
        warning('No Input specified')
        argument_parser.print_help()
        exit()

    # get the absolute path to the input file
    path_to_input = os.path.abspath(arguments.input)

    # read the eml file
    try:
        with open(path_to_input, mode='r') as input_file:
            eml_content = input_file.read()
    except Exception as e:
        error('Error: {}'.format(e))
        error('File could not be loaded')
        info('Existing')
        exit()

    # parse the eml file
    try:
        parsed_eml = message_from_string(eml_content)
    except Exception as e:
        error('Error: {}'.format(e))
        error('File could not be parsed. Sure it is a eml-file?')
        info('Existing')
        exit()

    # use default functionality if no options are specified
    is_default_functionality = not (arguments.header or arguments.tracking
                                    or arguments.attachments or arguments.text
                                    or arguments.html or arguments.structure
                                    or arguments.url
                                    or arguments.extract is not None)

    if is_default_functionality:
        arguments.structure = True
        arguments.url = True
        arguments.tracking = True
        arguments.attachments = True

    if arguments.header:
        show_header(parsed_eml=parsed_eml)
    if arguments.structure:
        show_structure(parsed_eml=parsed_eml)
    if arguments.url:
        show_urls(parsed_eml=parsed_eml)
    if arguments.tracking:
        check_tracking(parsed_eml=parsed_eml)
    if arguments.attachments:
        show_attachments(parsed_eml=parsed_eml)
    if arguments.text:
        show_text(parsed_eml=parsed_eml)
    if arguments.html:
        show_html(parsed_eml=parsed_eml)

    if arguments.extract is not None:
        extract_attachment(parsed_eml=parsed_eml,
                           attachment_number=arguments.extract,
                           output_path=arguments.output)
    if arguments.extract_all is not None:
        extract_all_attachments(parsed_eml=parsed_eml, path=arguments.output)
예제 #17
0
def main():
    parser = argparse.ArgumentParser(
        usage='showExposedGitHubEmails [OPTION]... -u USERNAME',
        description=
        'Lists information about the FILEs (the current directory by default) including Alternate Data Streams.'
    )
    parser.add_argument(
        '-u',
        '--user',
        dest="user",
        help="Username of the user which public repositories should be scanned",
        type=str)
    parser.add_argument('-r',
                        '--repository',
                        dest='repository',
                        help="check only one specific repository",
                        type=str)
    parser.add_argument(
        '-t',
        '--token',
        dest='token',
        help="Paste a GitHub token her to increase the API quota",
        type=str)
    parser.add_argument('-v',
                        '--verbose',
                        dest="verbose",
                        help="verbose mode",
                        action='store_true',
                        default=False)
    parser.add_argument('-d',
                        '--delay',
                        dest="delay",
                        help="The delay between to requests in seconds",
                        type=int,
                        default=None)
    parser.add_argument(
        '--api-url',
        dest="api_url",
        help='Specify the URL to the GitHub Api (default is "{}")'.format(
            API_URL),
        type=str,
        default=None)
    parser.add_argument('--no-forks',
                        dest="no_forks",
                        help='Ignore forked repositories',
                        action='store_true',
                        default=False)

    parsed_arguments = parser.parse_args()

    if parsed_arguments.user is None:
        warning('No username specified')
        parser.print_help()
        exit()

    if parsed_arguments.token is not None:
        update_header(
            {'Authorization': 'token {}'.format(parsed_arguments.token)})

    if parsed_arguments.delay is not None:
        set_delay(delay=parsed_arguments.delay)

    if parsed_arguments.api_url is not None:
        set_api_url(api_url=parsed_arguments.api_url)

    if parsed_arguments.verbose:
        set_verbosity_level(level=5)

    if parsed_arguments.repository is not None:
        repos_to_scan = [parsed_arguments.repository]
    else:
        info('Scan for public repositories of user {}'.format(
            parsed_arguments.user))
        repos_to_scan_sorted = sorted(
            get_all_repositories_of_a_user(username=parsed_arguments.user),
            key=lambda x: x.is_fork)
        repos_to_scan = [
            x.name for x in repos_to_scan_sorted
            if (parsed_arguments.no_forks and not x.is_fork)
            or not parsed_arguments.no_forks
        ]
        info('Found {} public repositories'.format(len(repos_to_scan)))

    emails_to_name = defaultdict(set)
    try:
        for repo in repos_to_scan:
            info('Scan repository {}'.format(repo))
            emails_to_name_new = get_all_emails_of_a_user(
                username=parsed_arguments.user, repo_name=repo)
            for email, names in emails_to_name_new.items():
                emails_to_name[email].update(names)
    except KeyboardInterrupt:
        warning('Keyboard interrupt. Stopped scanning.')

    if len(emails_to_name) > 0:
        max_width_email = max([len(x) for x in emails_to_name.keys()])
        info('Exposed emails and names:')
        for email, names in emails_to_name.items():
            names_string = ''
            for i, n in enumerate(names):
                names_string += colorize_string(n, Color.BLUE)
                if i < len(names) - 1:
                    names_string += '; '
            print(
                '\t',
                colorize_string(email.ljust(max_width_email), Color.RED) +
                ' - ' + names_string)
    else:
        info('No emails found')