diff --git a/pymisp/tools/reportlab_generator.py b/pymisp/tools/reportlab_generator.py index ce04f90..f0d3ee3 100644 --- a/pymisp/tools/reportlab_generator.py +++ b/pymisp/tools/reportlab_generator.py @@ -6,36 +6,31 @@ import base64 import logging import pprint from io import BytesIO +from pathlib import Path import sys import os if sys.version_info.major >= 3: from html import escape - # import PIL else: - print( - "ExportPDF running with Python < 3 : stability and output not guaranteed. Please run exportPDF with at least Python3") + print("ExportPDF running with Python < 3 : stability and output not guaranteed. Please run exportPDF with at least Python3") logger = logging.getLogger('pymisp') # Potentially not installed imports try: from reportlab.pdfgen import canvas - from reportlab.pdfbase.pdfmetrics import stringWidth, registerFont, registerFontFamily - from reportlab.pdfbase.pdfdoc import PDFDictionary, PDFInfo + from reportlab.pdfbase.pdfmetrics import stringWidth, registerFont from reportlab.pdfbase.ttfonts import TTFont from reportlab.lib import colors - from reportlab.lib.utils import ImageReader from reportlab.lib.pagesizes import A4 - from reportlab.lib.fonts import addMapping - from reportlab.platypus import SimpleDocTemplate, Paragraph, PageBreak, Spacer, Table, TableStyle, Flowable, Image, \ - Indenter + from reportlab.platypus import SimpleDocTemplate, Paragraph, PageBreak, Table, TableStyle, Flowable, Image, Indenter from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import mm - from reportlab.lib.enums import TA_RIGHT, TA_CENTER, TA_JUSTIFY, TA_LEFT + from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT HAS_REPORTLAB = True except ImportError: @@ -113,8 +108,7 @@ class Flowable_Tag(Flowable): LEFT_INTERNAL_PADDING = 2 ELONGATION = LEFT_INTERNAL_PADDING * 2 - p = Paragraph("" + self.text + "", - style=self.custom_style) + p = Paragraph("{}".format(self.choose_good_text_color(), self.text), style=self.custom_style) string_width = stringWidth(self.text, self.custom_style.fontName, self.custom_style.fontSize) self.width = string_width + ELONGATION @@ -197,14 +191,14 @@ EXTERNAL_ANALYSIS_PREFIX = "External analysis from an attribute : " # == Parameters for improvement of event's metadata == -threat_map = {"0": " undefined (0)", - "3": " Low (3)", - "2": " Medium (2)", - "1": " High (1)"} +threat_map = {"0": f" undefined (0)", + "3": f" Low (3)", + "2": f" Medium (2)", + "1": f" High (1)"} -analysis_map = {"0": " Initial (0)", - "1": " Ongoing (1)", - "2": " Completed (2)"} +analysis_map = {"0": f" Initial (0)", + "1": f" Ongoing (1)", + "2": f" Completed (2)"} # == Parameters for Sightings == POSITIVE_SIGHT_COLOR = 'green' @@ -260,11 +254,11 @@ def uuid_to_url(baseurl, uuid): ''' if baseurl[len(baseurl) - 1] != "/": baseurl += "/" - return baseurl + "events/view/" + uuid + return f"{baseurl}events/view/{uuid}" -def create_flowable_table_from_data(data, col_w=COL_WIDTHS, color_alternation=None, line_alternation=None, - galaxy_colors=False): +def create_flowable_table_from_data(data, col_w=COL_WIDTHS, color_alternation=None, + line_alternation=None, galaxy_colors=False): ''' Given a list of flowables items (2D/list of list), creates a Table with styles. :param data: list of list of items (flowables is better) @@ -353,8 +347,7 @@ def lines_style_generator(data, line_alternation): # For each line, generate a tuple giving to a line a color for each in range(data_len): - if each == 0 or line_alternation[each % len(line_alternation)] != line_alternation[ - (each - 1) % len(line_alternation)]: + if each == 0 or line_alternation[each % len(line_alternation)] != line_alternation[(each - 1) % len(line_alternation)]: lines_list.append(('LINEABOVE', (0, each), (-1, each), LINE_THICKNESS, LINE_COLOR)) # Last line @@ -385,7 +378,7 @@ def internationalize_font(config=None): global FIRST_COL_FONT global SECOND_COL_FONT - if is_in_config(config, 6) and config[moduleconfig[6]] != "" : + if is_in_config(config, 6) and config[moduleconfig[6]] != "": # Handle custom fonts. Has to be TrueType = one TTF only fonts_path_custom = config[moduleconfig[6]] @@ -395,11 +388,10 @@ def internationalize_font(config=None): SECOND_COL_FONT = 'custom_font' else: - logger.error( - "Trying to load a custom font, unable to access the file. Path : " + str(fonts_path_custom)) + logger.error(f"Trying to load a custom font, unable to access the file. Path: {fonts_path_custom}") else: ''' Handle provided NOTO fonts (CJK only for now) - # Available fonts : + # Available fonts : NotoSansCJKtc - DemiLight.ttf NotoSansCJKtc - Regular.ttf NotoSansCJKtc - Black.ttf @@ -408,21 +400,19 @@ def internationalize_font(config=None): NotoSansCJKtc - Bold.ttf NotoSansCJKtc - Medium.ttf ''' - font_path = os.path.join(os.path.abspath(os.path.dirname(sys.modules['pymisp'].__file__)), 'tools', 'pdf_fonts', - 'Noto_TTF') + font_path = Path(sys.modules['pymisp'].__file__).parent / 'tools' / 'pdf_fonts' / 'Noto_TTF' - noto_bold = font_path + "/NotoSansCJKtc-Bold.ttf" - noto = font_path + "/NotoSansCJKtc-DemiLight.ttf" + noto_bold = font_path / "NotoSansCJKtc-Bold.ttf" + noto = font_path / "NotoSansCJKtc-DemiLight.ttf" - if os.path.isfile(noto_bold) and os.path.isfile(noto): + if noto_bold.is_file() and noto.is_file(): registerFont(TTFont("Noto", noto)) registerFont(TTFont("Noto-bold", noto_bold)) FIRST_COL_FONT = 'Noto-bold' SECOND_COL_FONT = 'Noto' else: - logger.error( - "Trying to load a custom (internationalization) font, unable to access the file : " + noto_bold) + logger.error(f"Trying to load a custom (internationalization) font, unable to access the file: {noto_bold}") def get_table_styles(): @@ -485,18 +475,21 @@ def safe_string(bad_str): def is_safe_attribute(curr_object, attribute_name): - return hasattr(curr_object, attribute_name) and getattr(curr_object, attribute_name) is not None and getattr( - curr_object, attribute_name) != "" + return (hasattr(curr_object, attribute_name) + and getattr(curr_object, attribute_name) is not None + and getattr(curr_object, attribute_name) != "") def is_safe_dict_attribute(curr_object, attribute_name): - return attribute_name in curr_object and curr_object[attribute_name] is not None and curr_object[ - attribute_name] != "" + return (attribute_name in curr_object + and curr_object[attribute_name] is not None + and curr_object[attribute_name] != "") def is_safe_attribute_table(curr_object, attribute_name): - return hasattr(curr_object, attribute_name) and getattr(curr_object, attribute_name) is not None and getattr( - curr_object, attribute_name) != [] + return (hasattr(curr_object, attribute_name) + and getattr(curr_object, attribute_name) is not None + and getattr(curr_object, attribute_name) != []) def is_in_config(config, index): @@ -593,34 +586,37 @@ class Value_Formatter(): if curr_style is None: curr_style = self.col2_style - # Does MispEven has the attribute ? + escape = True + # Does MispEvent has the attribute ? if is_safe_attribute(misp_event, item[1]): # It has the requested attribute .. building upon it. # Does misp_object has an uuid and do we know the baseurl ? if is_safe_attribute(misp_event, "uuid") and is_in_config(self.config, 0): # We can build links + escape = False curr_uuid = str(getattr(misp_event, "uuid")) curr_baseurl = self.config[moduleconfig[0]] curr_url = uuid_to_url(curr_baseurl, curr_uuid) - html_url = "" + safe_string(getattr(misp_event, item[1])) + "" + html_url = "{}".format(curr_url, safe_string(getattr(misp_event, item[1]))) if color: # They want fancy colors - html_url = "" + html_url + "" - + html_url = f"{html_url}" # Construct final paragraph - answer = self.get_unoverflowable_paragraph(html_url, curr_style=curr_style, do_escape_string=False) + answer = html_url else: # We can't build links - answer = self.get_unoverflowable_paragraph(getattr(misp_event, item[1]), curr_style=curr_style) + answer = getattr(misp_event, item[1]) else: # No it doesn't, so we directly give the default answer - answer = self.get_unoverflowable_paragraph(item[2], curr_style=curr_style) + answer = item[2] - return answer + if not escape: + return self.get_unoverflowable_paragraph(answer, curr_style=curr_style, do_escape_string=False) + return self.get_unoverflowable_paragraph(answer, curr_style=curr_style) ######################################################################## # Specific attribute formater @@ -684,8 +680,7 @@ class Value_Formatter(): :return: a Paragraph to add in the pdf, regarding the values of "timestamp" ''' if is_safe_attribute(misp_event, item[1]): - return self.get_unoverflowable_paragraph( - safe_string(getattr(misp_event, item[1]).strftime(EXPORT_DATE_FORMAT))) + return self.get_unoverflowable_paragraph(safe_string(getattr(misp_event, item[1]).strftime(EXPORT_DATE_FORMAT))) return self.get_unoverflowable_paragraph(item[2]) def get_creator_organisation_value(self, misp_event, item): @@ -730,29 +725,28 @@ class Value_Formatter(): RED_COLOR = '#ff0000' GREEN_COLOR = '#008000' - YES_ANSWER = " Yes (" - NO_ANSWER = "No" + YES_ANSWER = f" Yes " + NO_ANSWER = f" No " # Formatting similar to MISP Event web view if is_safe_attribute(misp_event, item[1]): if getattr(misp_event, item[1]): # == True + answer = YES_ANSWER if is_safe_attribute(misp_event, item[3]): # Published and have published date - answer = self.get_unoverflowable_paragraph( - YES_ANSWER + getattr(misp_event, item[3]).strftime(EXPORT_DATE_FORMAT) + ")", - do_escape_string=False) + answer += '({})'.format(getattr(misp_event, item[3]).strftime(EXPORT_DATE_FORMAT)) else: # Published without published date - answer = self.get_unoverflowable_paragraph(YES_ANSWER + "no date)", do_escape_string=False) + answer += "(no date)" else: # Not published - answer = self.get_unoverflowable_paragraph(NO_ANSWER, do_escape_string=False) + answer = NO_ANSWER else: # Does not have a published attribute - answer = self.get_unoverflowable_paragraph(item[2], do_escape_string=False) + answer = item[2] - return answer + return self.get_unoverflowable_paragraph(answer, do_escape_string=False) def get_image_value(self, misp_attribute, item): ''' @@ -772,10 +766,8 @@ class Value_Formatter(): answer = img except OSError: - logger.error( - "Trying to add an attachment during PDF export generation. Attachement joining failed. Attachement may not be an image.") - answer = self.get_unoverflowable_paragraph( - "" + NOT_A_PICTURE_MESSAGE + "", do_escape_string=False) + logger.error("Trying to add an attachment during PDF export generation. Attachement joining failed. Attachement may not be an image.") + answer = self.get_unoverflowable_paragraph(f"{NOT_A_PICTURE_MESSAGE}", do_escape_string=False) return answer @@ -787,9 +779,7 @@ class Value_Formatter(): :param col2_style: style to be applied on the returned paragraph :return: a Paragraph to add in the pdf, regarding the values of this "link" attribute ''' - return self.get_unoverflowable_paragraph( - "" + getattr( - misp_attribute, item[1]) + "", do_escape_string=False) + return self.get_unoverflowable_paragraph(f"{getattr(misp_attribute, item[1])}", do_escape_string=False) def get_bad_link(self, misp_attribute, item): ''' @@ -799,11 +789,7 @@ class Value_Formatter(): :param col2_style: style to be applied on the returned paragraph :return: a Paragraph to add in the pdf, regarding the values of this "url" attribute ''' - return self.get_unoverflowable_paragraph( - "" + getattr(misp_attribute, - item[ - 1]) + "", - do_escape_string=False) + return self.get_unoverflowable_paragraph(f"{getattr(misp_attribute, item[1])}", do_escape_string=False) def get_good_or_bad_link(self, misp_attribute, item): ''' @@ -828,11 +814,12 @@ class Value_Formatter(): def get_galaxy_name_value(self, misp_galaxy): item = ["Name", 'name', "None", "namespace", "type"] if is_safe_dict_attribute(misp_galaxy, item[1]): - return self.get_unoverflowable_paragraph(safe_string(misp_galaxy[item[1]]) - + " from " + safe_string(misp_galaxy[item[3]]) + ":" - + safe_string(misp_galaxy[item[4]]), do_escape_string=False, - do_small=True) - return self.get_unoverflowable_paragraph(item[2], do_small=True) + to_return = '{} from {}:{}'.format(safe_string(misp_galaxy[item[1]]), + safe_string(misp_galaxy[item[3]]), + safe_string(misp_galaxy[item[4]])) + else: + to_return = item[2] + return self.get_unoverflowable_paragraph(to_return, do_small=True) def get_galaxy_cluster_name_value(self, misp_cluster, do_small=False): item = ["Name", 'value', "None", "source", "meta", "synonyms"] @@ -886,18 +873,18 @@ class Event_Metadata(): # Date item = ["Date", 'date', "None"] - data.append( - [self.value_formatter.get_col1_paragraph(item[0]), self.value_formatter.get_date_value(misp_event, item)]) + data.append([self.value_formatter.get_col1_paragraph(item[0]), + self.value_formatter.get_date_value(misp_event, item)]) # Owner item = ["Owner org", 'owner', "None"] - data.append( - [self.value_formatter.get_col1_paragraph(item[0]), self.value_formatter.get_owner_value(misp_event, item)]) + data.append([self.value_formatter.get_col1_paragraph(item[0]), + self.value_formatter.get_owner_value(misp_event, item)]) # Threat item = ["Threat level", 'threat_level_id', "None"] - data.append( - [self.value_formatter.get_col1_paragraph(item[0]), self.value_formatter.get_threat_value(misp_event, item)]) + data.append([self.value_formatter.get_col1_paragraph(item[0]), + self.value_formatter.get_threat_value(misp_event, item)]) # Analysis item = ["Analysis", 'analysis', "None"] @@ -988,23 +975,21 @@ class Event_Metadata(): ''' ''' - The event "{EventName}" | that occurred on {EventDate}, | had been shared by {Organisation Name} | on the {Date}. + The event "{EventName}" | that occurred on {EventDate}, | had been shared by {Organisation Name} | on the {Date}. ''' text = "" - item = ["Info", 'info', "None"] - if is_safe_attribute(misp_event, item[1]): + if is_safe_attribute(misp_event, 'info'): text += "The event '" - text += safe_string(getattr(misp_event, item[1])) + text += safe_string(misp_event.info) text += "'" else: text += "This event" - item = ["Event date", 'timestamp', "None"] - if is_safe_attribute(misp_event, item[1]): + if is_safe_attribute(misp_event, 'timestamp'): text += " that occurred on " - text += safe_string(getattr(misp_event, item[1]).strftime(EXPORT_DATE_FORMAT)) + text += safe_string(misp_event.timestamp.strftime(EXPORT_DATE_FORMAT)) text += "," item = ["Creator Org", 'Orgc', "None", "name"] @@ -1023,7 +1008,7 @@ class Event_Metadata(): text += "." ''' - The threat level of this event is {ThreatLevel} and the analysis that was made of this event is {AnalysisLevel}. + The threat level of this event is {ThreatLevel} and the analysis that was made of this event is {AnalysisLevel}. ''' item = ["Threat level", 'threat_level_id', "None"] @@ -1176,7 +1161,7 @@ class Attributes(): # Handle the special case of links STANDARD_TYPE = True - if is_safe_attribute(misp_attribute, 'type') and (getattr(misp_attribute, 'type') in [LINK_TYPE, URL_TYPE]): + if is_safe_attribute(misp_attribute, 'type') and (misp_attribute.type in [LINK_TYPE, URL_TYPE]): # getattr(misp_attribute, 'type') == LINK_TYPE or getattr(misp_attribute, 'type') == URL_TYPE): # Special case for links STANDARD_TYPE = False @@ -1241,10 +1226,11 @@ class Attributes(): # There is some attributes for this object for attribute in getattr(misp_event, "Attribute"): # If the current event is an external analysis and a comment - if is_safe_attribute(attribute, "value") and is_safe_attribute(attribute, - "category") and is_safe_attribute( - attribute, "type") and getattr(attribute, "category") == "External analysis" and getattr( - attribute, "type") == "comment": + if (is_safe_attribute(attribute, "value") + and is_safe_attribute(attribute, "category") + and is_safe_attribute(attribute, "type") + and getattr(attribute, "category") == "External analysis" + and getattr(attribute, "type") == "comment"): # We add it to the description text += "
" + EXTERNAL_ANALYSIS_PREFIX + safe_string(getattr(attribute, "value")) @@ -1343,10 +1329,9 @@ class Sightings(): i += 1 # Create the sighting text - sight_text = " Positive : " + str(list_sighting[0]) + "" - sight_text += " / " + " Negative : " + str( - list_sighting[1]) + "" - sight_text += " / " + " Misc. : " + str(list_sighting[2]) + "" + sight_text = f" Positive: {list_sighting[0]}" + sight_text += f" / Negative: {list_sighting[1]}" + sight_text += f" / Misc.: {list_sighting[2]}" answer_sighting = self.value_formatter.get_unoverflowable_paragraph(sight_text, do_escape_string=False) else: @@ -1516,7 +1501,6 @@ class Galaxy(): clusters_metadata = curr_cluster.create_flowable_table_from_galaxy_clusters(curr_galaxy) flowable_table += clusters_metadata - else: # No galaxies for this object answer_tags = [self.value_formatter.get_unoverflowable_paragraph("No galaxies")] @@ -1658,22 +1642,20 @@ class Statics_Drawings(): ''' if is_safe_attribute(self.misp_event, 'info'): - canvas.setTitle(getattr(self.misp_event, 'info')) - - if is_safe_attribute(self.misp_event, 'info'): - canvas.setSubject(getattr(self.misp_event, 'info')) + canvas.setTitle(self.misp_event.info) + canvas.setSubject(self.misp_event.info) if is_safe_attribute(self.misp_event, 'Orgc'): - if is_safe_attribute(getattr(self.misp_event, 'Orgc'), 'name'): - canvas.setAuthor(getattr(getattr(self.misp_event, 'Orgc'), 'name')) + if is_safe_attribute(self.misp_event.Orgc, 'name'): + canvas.setAuthor(self.misp_event.Orgc.name) if is_in_config(self.config, 1): canvas.setCreator(self.config[moduleconfig[1]]) else: - canvas.setCreator(getattr(getattr(self.misp_event, 'Orgc'), 'name')) + canvas.setCreator(self.misp_event.Orgc.name) if is_safe_attribute(self.misp_event, 'uuid'): - canvas.setKeywords(getattr(self.misp_event, 'uuid')) + canvas.setKeywords(self.misp_event.uuid) def add_page_number(self, canvas, doc): ''' @@ -1716,8 +1698,8 @@ def collect_parts(misp_event, config=None): curr_val_f = Value_Formatter(config, col1_style, col2_style, col1_small_style, col2_small_style) # Create stuff - title_style = ParagraphStyle(name='Column_1', parent=sample_style_sheet['Heading1'], fontName=FIRST_COL_FONT, - alignment=TA_CENTER) + title_style = ParagraphStyle(name='Column_1', parent=sample_style_sheet['Heading1'], + fontName=FIRST_COL_FONT, alignment=TA_CENTER) title = curr_val_f.get_value_link_to_event(misp_event, ["Info", 'info', "None"], title_style, color=False) # Add all parts to final PDF flowables.append(title)