From 2adac8f0d2ee1908edfb10057cf1e14e10099151 Mon Sep 17 00:00:00 2001 From: Falconieri Date: Mon, 25 Feb 2019 15:49:53 +0100 Subject: [PATCH] chg: [exportpdf] Add metadata, bugfixes cases (too long values, sanitization), links to misp instances --- pymisp/tools/reportlab_generator.py | 981 +++++++++++--------- tests/reportlab_testoutputs/basic_event.pdf | 391 -------- tests/test_reportlab.py | 148 ++- 3 files changed, 677 insertions(+), 843 deletions(-) delete mode 100644 tests/reportlab_testoutputs/basic_event.pdf diff --git a/pymisp/tools/reportlab_generator.py b/pymisp/tools/reportlab_generator.py index dd4e0d8..b84f3ac 100644 --- a/pymisp/tools/reportlab_generator.py +++ b/pymisp/tools/reportlab_generator.py @@ -6,8 +6,15 @@ import base64 import logging import pprint from io import BytesIO -import pymisp -from html import escape +from functools import partial + +import sys + +if sys.version_info.major >= 3: + from html import escape +else: + print( + "ExportPDF running with Python < 3 : stability and output not guaranteed. Please run exportPDF with at least Python3") logger = logging.getLogger('pymisp') @@ -31,9 +38,20 @@ except ImportError: ######################################################################## +def create_flowable_tag(misp_tag): + ''' + Returns a Flowable tag linked to one tag. + :param misp_tag: A misp tag of a misp event or a misp event's attribute + :return: one flowable representing a tag (with style) + ''' + col1_style, col2_style = get_table_styles() + + return [Flowable_Tag(text=misp_tag.name, color=misp_tag.colour, custom_style=col1_style)] + + class Flowable_Tag(Flowable): """ - Custom flowable to handle tags + Custom flowable to handle tags. Draw one Tag with the webview formatting Modified from : http://two.pairlist.net/pipermail/reportlab-users/2005-February/003695.html and : http://www.blog.pythonlibrary.org/2014/03/10/reportlab-how-to-create-custom-flowables/ """ @@ -74,16 +92,18 @@ class Flowable_Tag(Flowable): brightness = r * 299 + g * 587 + b * 114 / 1000 if brightness < 500: # Standard treeshold for human vision : 123 instead of 500 - return "#ffffff" # Black + text_color = "#ffffff" # Black else: - return "#000000" # White + text_color = "#000000" # White + + return text_color # ---------------------------------------------------------------------- def draw(self): - """ + ''' Draw the shape, text, etc to show a Tag Honestely, constant are totally ad-hoc. Feels free to change it, but be sure to test the visual result of it. - """ + ''' RADIUS = 1 * mm LEFT_INTERNAL_PADDING = 2 ELONGATION = LEFT_INTERNAL_PADDING * 2 @@ -102,6 +122,9 @@ class Flowable_Tag(Flowable): p.drawOn(self.canv, *self.coord(self.x, self.y + 0.5 * LEFT_INTERNAL_PADDING, mm)) +# Copy of pdfexport.py moduleconfig +moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata"] + # == Row colors of the table (alternating) == EVEN_COLOR = colors.whitesmoke ODD_COLOR = colors.lightgrey @@ -132,15 +155,20 @@ PAGESIZE = (140 * mm, 216 * mm) # width, height BASE_MARGIN = 5 * mm # Create a list here to specify each row separately # == Parameters for error handling for content too long to fit on a page == -FRAME_MAX_HEIGHT = 500 # 650 # Ad hoc value for a A4 page +FRAME_MAX_HEIGHT = 500 # 650 # Ad hoc value for a A4 page FRAME_MAX_WIDTH = 356 STR_TOO_LONG_WARNING = "
[Too long to fit on a single page. Cropped]" +# == Parameters for links management == +LINK_TYPE = "link" # Name of the type that define 'good' links +URL_TYPE = "url" # Name of the type that define 'bad' links +WARNING_MESSAGE_URL = "'https://Please_consider_that_this_may_be_a_harmful_link'" +GOOD_LINK_COLOR = 'blue' +BAD_LINK_COLOR = 'red' -''' -"UTILITIES" METHODS. Not meant to be used except for development purposes -''' +######################################################################## +# "UTILITIES" METHODS. Not meant to be used except for development purposes def get_sample_fonts(): ''' @@ -168,10 +196,7 @@ def get_sample_styles(): sample_style_sheet.list() -''' -"INTERNAL" METHODS. Not meant to be used outside of this class. -''' - +# "INTERNAL" METHODS. Not meant to be used outside of this class. def alternate_colors_style_generator(data): ''' @@ -234,348 +259,6 @@ def general_style_generator(): return lines_list -def get_published_value(misp_event, item, col2_style): - ''' - Returns a flowable paragraph to add to the pdf given the misp_event published/published_time - More information on how to play with paragraph into reportlab cells : - https://stackoverflow.com/questions/11810008/reportlab-add-two-paragraphs-into-one-table-cell - :param misp_event: A misp event with or without "published"/"publish_timestamp" attributes - :param item: a list of name, in order : - ["Name to be print in the pdf", "json property access name", - " Name to be display if no values found in the misp_event", json property access name (for timestamp")] - e.g. item = ["Published", 'published', "None", "publish_timestamp"] - :param col2_style: style to be applied on the returned paragraph - :return: a Paragraph to add in the pdf, regarding the values of "published"/"publish_timestamp" - ''' - - RED_COLOR = '#ff0000' - GREEN_COLOR = '#008000' - YES_ANSWER = " Yes (" - NO_ANSWER = "No" - - # Formatting similar to MISP Event web view - if hasattr(misp_event, item[1]): - if getattr(misp_event, item[1]): # == True - if hasattr(misp_event, item[3]): - # Published and have published date - return Paragraph(YES_ANSWER + getattr(misp_event, item[3]).strftime(EXPORT_DATE_FORMAT) + ")", - col2_style) - else: - # Published without published date - return YES_ANSWER + "no date)" - else: - # Not published - return NO_ANSWER - else: - # Does not have a published attribute - return item[2] - - -def get_timestamp_value(misp_event, item, col2_style): - ''' - Returns a flowable paragraph to add to the pdf given the misp_event timestamp - :param misp_event: A misp event with or without "timestamp" attributes - :param item: a list of name, in order : - ["Name to be print in the pdf", "json property access name", - " Name to be display if no values found in the misp_event"] - :param col2_style: style to be applied on the returned paragraph - :return: a Paragraph to add in the pdf, regarding the values of "timestamp" - ''' - if hasattr(misp_event, item[1]): - return Paragraph(str(getattr(misp_event, item[1]).strftime(EXPORT_DATE_FORMAT)), col2_style) - else: - return Paragraph(item[2], col2_style) - - -def get_creator_organisation_value(misp_event, item, col2_style): - ''' - Returns a flowable paragraph to add to the pdf given the misp_event creator organisation - :param misp_event: A misp event with or without "timestamp" attributes - :param item: a list of name, in order : - ["Name to be print in the pdf", "json property access name", - " Name to be display if no values found in the misp_event", "json property access name (second level)"] - :param col2_style: style to be applied on the returned paragraph - :return: a Paragraph to add in the pdf, regarding the values of "creator organisation" - ''' - if hasattr(misp_event, item[1]): - return Paragraph(escape(str(getattr(getattr(misp_event, item[1]), item[3]))), col2_style) - else: - return Paragraph(item[2], col2_style) - - -def get_attributes_number_value(misp_event, item, col2_style): - ''' - Returns a flowable paragraph to add to the pdf given the misp_event attributes - :param misp_event: A misp event with or without "attributes" attributes - :param item: a list of name, in order : - ["Name to be print in the pdf", "json property access name", - " Name to be display if no values found in the misp_event"] - :param col2_style: style to be applied on the returned paragraph - :return: a Paragraph to add in the pdf, regarding the values of "attributes" - ''' - if hasattr(misp_event, item[1]): - return Paragraph(str(len(getattr(misp_event, item[1]))), col2_style) - else: - return Paragraph(item[2], col2_style) - - -def get_tag_value(misp_event, item, col2_style): - ''' - Returns a flowable paragraph to add to the pdf given the misp_event tags - :param misp_event: A misp event with or without "tags" attributes - :param item: a list of name, in order : - ["Name to be print in the pdf", "json property access name", - " Name to be display if no values found in the misp_event"] - :param col2_style: style to be applied on the returned paragraph - :return: a Paragraph to add in the pdf, regarding the values of "tags" - ''' - if hasattr(misp_event, item[1]): - table_event_tags = create_flowable_table_from_tags(misp_event) - return table_event_tags - else: - return Paragraph(item[2], col2_style) - -def get_unoverflowable_paragraph(dirty_string, curr_style) : - ''' - Create a paragraph that can fit on a cell of one page. Mostly hardcoded values. - This method can be improved (get the exact size of the current frame, and limit the paragraph to this size.) - This might be worst look at KeepInFrame (which hasn't went well so far) - :param dirty_string: - :param curr_style: - :return: - ''' - sanitized_str = str(escape(dirty_string)) - - # Get the space that the paragraph needs to be printed - w, h = Paragraph(sanitized_str, curr_style).wrap(FRAME_MAX_WIDTH, FRAME_MAX_HEIGHT) - - # If there is enough space, directly send back the sanitized paragraph - if w <= FRAME_MAX_WIDTH and h <= FRAME_MAX_HEIGHT : - return Paragraph(sanitized_str, curr_style) - else : - # Otherwise, cut the content to fit the paragraph (Dichotomy) - max_carac_amount = int((FRAME_MAX_HEIGHT/(h*1.0))*len(sanitized_str)) - - i = 0 - MAX_ITERATION = 10 - limited_string = "" - while (w > FRAME_MAX_WIDTH or h > FRAME_MAX_HEIGHT) and i" + safe_string(getattr(misp_event, item[1])) + "" + + if color: + # They want fancy colors + html_url = "" + html_url + "" + + # Construct final paragraph + answer = get_unoverflowable_paragraph(html_url, col2_style, False) + + else: + # We can't build links + answer = get_unoverflowable_paragraph(getattr(misp_event, item[1]), col2_style) + + else: + # No it doesn't, so we directly give the default answer + answer = Paragraph(item[2], col2_style) + + return answer + + +def get_timestamp_value(misp_event, item, col2_style): + ''' + Returns a flowable paragraph to add to the pdf given the misp_event timestamp + :param misp_event: A misp event with or without "timestamp" attributes + :param item: a list of name, in order : + ["Name to be print in the pdf", "json property access name", + " Name to be display if no values found in the misp_event"] + :param col2_style: style to be applied on the returned paragraph + :return: a Paragraph to add in the pdf, regarding the values of "timestamp" + ''' + if hasattr(misp_event, item[1]): + return Paragraph(str(getattr(misp_event, item[1]).strftime(EXPORT_DATE_FORMAT)), col2_style) + return Paragraph(item[2], col2_style) + + +def get_creator_organisation_value(misp_event, item, col2_style): + ''' + Returns a flowable paragraph to add to the pdf given the misp_event creator organisation + :param misp_event: A misp event with or without "timestamp" attributes + :param item: a list of name, in order : + ["Name to be print in the pdf", "json property access name", + " Name to be display if no values found in the misp_event", "json property access name (second level)"] + :param col2_style: style to be applied on the returned paragraph + :return: a Paragraph to add in the pdf, regarding the values of "creator organisation" + ''' + if hasattr(misp_event, item[1]): + return Paragraph(safe_string(getattr(getattr(misp_event, item[1]), item[3])), col2_style) + return Paragraph(item[2], col2_style) + + +def get_attributes_number_value(misp_event, item, col2_style): + ''' + Returns a flowable paragraph to add to the pdf given the misp_event attributes + :param misp_event: A misp event with or without "attributes" attributes + :param item: a list of name, in order : + ["Name to be print in the pdf", "json property access name", + " Name to be display if no values found in the misp_event"] + :param col2_style: style to be applied on the returned paragraph + :return: a Paragraph to add in the pdf, regarding the values of "attributes" + ''' + if hasattr(misp_event, item[1]): + return Paragraph(str(len(getattr(misp_event, item[1]))), col2_style) + return Paragraph(item[2], col2_style) + + +def get_tag_value(misp_event, item, col2_style): + ''' + Returns a flowable paragraph to add to the pdf given the misp_event tags + :param misp_event: A misp event with or without "tags" attributes + :param item: a list of name, in order : + ["Name to be print in the pdf", "json property access name", + " Name to be display if no values found in the misp_event"] + :param col2_style: style to be applied on the returned paragraph + :return: a Paragraph to add in the pdf, regarding the values of "tags" + ''' + if hasattr(misp_event, item[1]): + table_event_tags = create_flowable_table_from_tags(misp_event) + return table_event_tags + return Paragraph(item[2], col2_style) + + +def get_published_value(misp_event, item, col2_style): + ''' + Returns a flowable paragraph to add to the pdf given the misp_event published/published_time + More information on how to play with paragraph into reportlab cells : + https://stackoverflow.com/questions/11810008/reportlab-add-two-paragraphs-into-one-table-cell + :param misp_event: A misp event with or without "published"/"publish_timestamp" attributes + :param item: a list of name, in order : + ["Name to be print in the pdf", "json property access name", + " Name to be display if no values found in the misp_event", json property access name (for timestamp")] + e.g. item = ["Published", 'published', "None", "publish_timestamp"] + :param col2_style: style to be applied on the returned paragraph + :return: a Paragraph to add in the pdf, regarding the values of "published"/"publish_timestamp" + ''' + + RED_COLOR = '#ff0000' + GREEN_COLOR = '#008000' + YES_ANSWER = " Yes (" + NO_ANSWER = "No" + + answer = "" + + # Formatting similar to MISP Event web view + if hasattr(misp_event, item[1]): + if getattr(misp_event, item[1]): # == True + if hasattr(misp_event, item[3]): + # Published and have published date + answer = Paragraph(YES_ANSWER + getattr(misp_event, item[3]).strftime(EXPORT_DATE_FORMAT) + ")", + col2_style) + else: + # Published without published date + answer = YES_ANSWER + "no date)" + else: + # Not published + answer = NO_ANSWER + else: + # Does not have a published attribute + answer = item[2] + + return answer + + +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) != "" + + +def create_flowable_table_from_one_attribute(misp_attribute): + ''' + Returns a table (flowalbe) representing the attribute + :param misp_attribute: A misp attribute + :return: a table representing this misp's attribute's attributes, to add to the pdf as a flowable + ''' + data = [] + col1_style, col2_style = get_table_styles() + + # To reduce code size, and automate it a bit, triplet (Displayed Name, object_attribute_name, + # to_display_if_not_present) are store in the following list + list_attr_automated = [["UUID", 'uuid', "None"], + ["Category", 'category', "None"], + ["Comment", 'comment', "None"], + ["Type", 'type', "None"], + ["Value", 'value', "None"]] + + # Handle the special case of links + STANDARD_TYPE = True + if hasattr(misp_attribute, 'type') and ( + getattr(misp_attribute, 'type') == LINK_TYPE or getattr(misp_attribute, 'type') == URL_TYPE): + # Special case for links + STANDARD_TYPE = False + + # Automated adding of standard (python) attributes of the misp event + for item in list_attr_automated: + if is_safe_attribute(misp_attribute, item[1]) and (STANDARD_TYPE or item[1] != 'value'): + # The attribute exists, we fetch it and create the row + data.append([Paragraph(item[0], col1_style), + get_unoverflowable_paragraph(getattr(misp_attribute, item[1]), col2_style)]) + + # The attribute does not exist, you may want to print a default text on the row. Then use as a else case : + # data.append([Paragraph(item[0], col1_style), Paragraph(item[2], col2_style)]) + + # Handle Special case for links (Value) + if not STANDARD_TYPE: + item = ["Value", 'value', "None"] + + if is_safe_attribute(misp_attribute, item[1]): + if getattr(misp_attribute, 'type') == LINK_TYPE: + data.append([Paragraph(item[0], col1_style), get_unoverflowable_paragraph( + "" + getattr( + misp_attribute, item[1]) + "", col2_style, False)]) + elif getattr(misp_attribute, 'type') == URL_TYPE: + data.append([Paragraph(item[0], col1_style), get_unoverflowable_paragraph( + "" + getattr(misp_attribute, + item[ + 1]) + "", + col2_style, False)]) + + # Tags + item = ["Tags", 'Tag', "None"] + if hasattr(misp_attribute, item[1]): + data.append([Paragraph(item[0], col1_style), get_tag_value(misp_attribute, item, col2_style)]) + + return create_flowable_table_from_data(data) + + +def create_tags_table_from_data(data): + ''' + Given a list of flowables tags (2D/list of list), creates a Table with styles adapted to tags. + :param data: list of list of tags (flowables) + :return: a Table - with styles - to add to another table + ''' + + # Create the table + curr_table = Table(data, COL_WIDTHS, rowHeights=ROW_HEIGHT_FOR_TAGS) + + # Create styles and set parameters + general_style = general_style_generator() + + # Make the table nicer + curr_table.setStyle(TableStyle(general_style)) + + return curr_table + + +######################################################################## +# General attribut formater + +def safe_string(bad_str): + return escape(str(bad_str)) + + +def get_unoverflowable_paragraph(dirty_string, curr_style, do_escape_string=True): + ''' + Create a paragraph that can fit on a cell of one page. Mostly hardcoded values. + This method can be improved (get the exact size of the current frame, and limit the paragraph to this size.) + This might be worst look at KeepInFrame (which hasn't went well so far) + :param do_escape_string: Activate the escaping (may be useful to add inline HTML, e.g. hyperlinks) + :param dirty_string: String to transform + :param curr_style: Style to apply to the returned paragraph + :return: + ''' + if do_escape_string: + sanitized_str = str(escape(str(dirty_string))) + else: + sanitized_str = dirty_string + + # Get the space that the paragraph needs to be printed + w, h = Paragraph(sanitized_str, curr_style).wrap(FRAME_MAX_WIDTH, FRAME_MAX_HEIGHT) + + # If there is enough space, directly send back the sanitized paragraph + if w <= FRAME_MAX_WIDTH and h <= FRAME_MAX_HEIGHT: + answer_paragraph = Paragraph(sanitized_str, curr_style) + else: + # Otherwise, cut the content to fit the paragraph (Dichotomy) + max_carac_amount = int((FRAME_MAX_HEIGHT / (h * 1.0)) * len(sanitized_str)) + + i = 0 + MAX_ITERATION = 10 + limited_string = "" + while (w > FRAME_MAX_WIDTH or h > FRAME_MAX_HEIGHT) and i < MAX_ITERATION: + i += 1 + limited_string = sanitized_str[:max_carac_amount] # .replace("\n", "").replace("\r", "") + w, h = Paragraph(limited_string + STR_TOO_LONG_WARNING, curr_style).wrap(FRAME_MAX_WIDTH, FRAME_MAX_HEIGHT) + max_carac_amount = int(max_carac_amount / 2) + + if w <= FRAME_MAX_WIDTH and h <= FRAME_MAX_HEIGHT: + answer_paragraph = Paragraph(limited_string + STR_TOO_LONG_WARNING, curr_style) + else: + # We may still end with a not short enough string + answer_paragraph = Paragraph(STR_TOO_LONG_WARNING, curr_style) + + return answer_paragraph + + +######################################################################## +# General Event's Attributes formater tools + +def uuid_to_url(baseurl, uuid): + ''' + Return an url constructed from the MISP baseurl and the uuid of the event, to go to this event on this MISP + :param baseurl: the baseurl of the MISP instnce e.g. http://localhost:8080 or http://localhost:8080/ + :param uuid: the uuid of the event that we want to have a link to + :return: the complete URL to go to this event on this MISP instance + ''' + if baseurl[len(baseurl) - 1] != "/": + baseurl += "/" + return baseurl + "events/view/" + uuid + + +def create_flowable_table_from_data(data): + ''' + 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) + :return: a Table - with styles - to add to the pdf + ''' + # Create the table + curr_table = Table(data, COL_WIDTHS) + + # Aside notes : + # colWidths='*' does a 100% and share the space automatically + # rowHeights=ROW_HEIGHT if you want a fixed height. /!\ Problems with paragraphs that are spreading everywhere + + # Create styles and set parameters + alternate_colors_style = alternate_colors_style_generator(data) + lines_style = lines_style_generator(data) + general_style = general_style_generator() + + # Make the table nicer + curr_table.setStyle(TableStyle(general_style + alternate_colors_style + lines_style)) + + return curr_table + + +######################################################################## +# General Event's Attributes formater + +def create_flowable_table_from_event(misp_event, config=None): + ''' + Returns Table presenting a MISP event + :param misp_event: A misp event (complete or not) + :return: a table that can be added to a pdf + ''' + + # To reduce code size, and automate it a bit, triplet (Displayed Name, object_attribute_name, + # to_display_if_not_present) are store in the following list + list_attr_automated = [ + # ["Event ID", 'id', "None"], + ["Date", 'date', "None"], + ["Owner org", 'owner', "None"], + ["Threat level", 'threat_level_id', "None"], # TODO : improve design + ["Analysis", 'analysis', "None"], # TODO : improve design + Ask where the enum is ! + # TODO : Not present ["Email", 'email', "None"], + # TODO : ["Distribution", 'distribution', "None"], + # TODO : ["First recorded change", 'TODO', "None"], + # TODO : ["Last change", 'TODO', "None"], + # TODO : ["Modification map", 'TODO', "None"], + # TODO : ["Sightings", 'TODO', "None"] + ] + + data = [] + col1_style, col2_style = get_table_styles() + + # Manual addition + # UUID + item = ["UUID", 'uuid', "None"] + data.append([Paragraph(item[0], col1_style), get_value_link_to_event(misp_event, item, col2_style, config)]) + + # Automated adding of standard (python) attributes of the misp event + # Note that PEP 0363 may change the syntax in future release : https://www.python.org/dev/peps/pep-0363/ + for item in list_attr_automated: + if hasattr(misp_event, item[1]): + # The attribute exist, we fetch it and create the row + data.append( + [Paragraph(item[0], col1_style), + get_unoverflowable_paragraph(getattr(misp_event, item[1]), col2_style)]) + else: + # The attribute does not exist ,we print a default text on the row + data.append([Paragraph(item[0], col1_style), Paragraph(item[2], col2_style)]) + + # Manual addition + # Info + item = ["Info", 'info', "None"] + data.append([Paragraph(item[0], col1_style), get_value_link_to_event(misp_event, item, col2_style, config)]) + + # Timestamp + item = ["Event date", 'timestamp', "None"] + data.append([Paragraph(item[0], col1_style), get_timestamp_value(misp_event, item, col2_style)]) + + # Published + item = ["Published", 'published', "None", "publish_timestamp"] + data.append([Paragraph(item[0], col1_style), get_published_value(misp_event, item, col2_style)]) + + # Creator organisation + item = ["Creator Org", 'Orgc', "None", "name"] + data.append([Paragraph(item[0], col1_style), get_creator_organisation_value(misp_event, item, col2_style)]) + + # Number of Attributes + item = ["# Attributes", 'Attribute', "None"] + data.append([Paragraph(item[0], col1_style), get_attributes_number_value(misp_event, item, col2_style)]) + + # Tags + item = ["Tags", 'Tag', "None"] + data.append([Paragraph(item[0], col1_style), get_tag_value(misp_event, item, col2_style)]) + + return create_flowable_table_from_data(data) + + +def create_flowable_table_from_attributes(misp_event): + ''' + Returns a list of flowables representing the list of attributes of a misp event. + The list is composed alternatively of headers and tables, to add to the pdf + :param misp_event: A misp event + :return: a table of flowables + ''' + + flowable_table = [] sample_style_sheet = getSampleStyleSheet() + i = 0 - set_metadata(misp_event) + if hasattr(misp_event, "Attribute"): + # There is some attributes for this object + for item in getattr(misp_event, "Attribute"): + # you can use a spacer instead of title to separate paragraph: flowable_table.append(Spacer(1, 5 * mm)) + flowable_table.append(Paragraph("Attribute #" + str(i), sample_style_sheet['Heading3'])) + flowable_table.append(create_flowable_table_from_one_attribute(item)) + i += 1 + else: + # No attributes for this object + flowable_table.append(Paragraph("No attributes", sample_style_sheet['Heading2'])) - # Create stuff - title = Paragraph(misp_event.info, sample_style_sheet['Heading1']) - subtitle = Paragraph("General information", sample_style_sheet['Heading2']) - attributes = Paragraph("Attributes", sample_style_sheet['Heading2']) - - table_event_general = create_flowable_table_from_event(misp_event) - table_event_attribute = create_flowable_table_from_attributes(misp_event) - - # If you want to output the full json, just add next line - # paragraph_2 = Paragraph(str(misp_event.to_json()), sample_style_sheet['Code']) - - # Add all parts to final PDF - flowables.append(title) - flowables.append(subtitle) - flowables.append(table_event_general) - - flowables.append(PageBreak()) - - flowables.append(attributes) - flowables += table_event_attribute - - return flowables + return flowable_table -def set_template(canvas, doc): +def create_flowable_table_from_tags(misp_event): + ''' + Returns a Table (flowable) to add to a pdf, representing the list of tags of an event or a misp event + :param misp_event: A misp event + :return: a table of flowable to add to the pdf + ''' + + flowable_table = [] + col1_style, col2_style = get_table_styles() + i = 0 + + if hasattr(misp_event, "Tag") and len(getattr(misp_event, "Tag")) > 1: # 'Tag' can exist and be empty + # There is some tags for this object + for item in getattr(misp_event, "Tag"): + flowable_table.append(create_flowable_tag(item)) + i += 1 + answer_tags = create_tags_table_from_data(flowable_table) + else: + # No tags for this object + answer_tags = [Paragraph("No tags", col2_style)] + + return answer_tags + + +######################################################################## +# Handling static parts drawn on the upper layer + +def set_template(canvas, doc, misp_event, config=None): add_page_number(canvas, doc) - add_metadata(canvas, doc) + add_metadata(canvas, doc, misp_event, config) + # TODO : add_header() + # TODO : add_footer() + + +def add_metadata(canvas, doc, misp_event, config=None): + ''' + Allow to add metadata to the pdf. Would need deeper digging to change other metadata. + :param canvas: / Automatically filled during pdf compilation + :param doc: / Automatically filled during pdf compilation + :param misp_event: To send trough "partial", to get information to complete metadaa + :return: / Automatically filled during pdf compilation + ''' -METADATA = {} -def set_metadata(misp_event: pymisp.MISPEvent): if hasattr(misp_event, 'info'): - METADATA["title"] = getattr(misp_event, 'info') + canvas.setTitle(getattr(misp_event, 'info')) + if hasattr(misp_event, 'info'): - METADATA["subject"] = getattr(misp_event, 'info') + canvas.setSubject(getattr(misp_event, 'info')) + if hasattr(misp_event, 'Orgc'): if hasattr(getattr(misp_event, 'Orgc'), 'name'): - METADATA["author"] = getattr(getattr(misp_event, 'Orgc'), 'name') - METADATA["creator"] = getattr(getattr(misp_event, 'Orgc'), 'name') - if hasattr(misp_event, 'uuid'): - METADATA["keywords"] = getattr(misp_event, 'uuid') + canvas.setAuthor(getattr(getattr(misp_event, 'Orgc'), 'name')) + + if config is not None and moduleconfig[1] in config: + canvas.setCreator(config[moduleconfig[1]]) + else: + canvas.setCreator(getattr(getattr(misp_event, 'Orgc'), 'name')) + + if hasattr(misp_event, 'uuid'): + canvas.setKeywords(getattr(misp_event, 'uuid')) -def add_metadata(canvas, doc): - # There should be a nicer way to do it : - # From : https://stackoverflow.com/questions/52358853/reportlab-metadata-creationdate-and-modificationdate - keys = METADATA.keys() - if 'title' in keys: - canvas.setTitle(METADATA["title"]) - if 'subject' in keys: - canvas.setSubject(METADATA["subject"]) - if 'author' in keys: - canvas.setAuthor(METADATA["author"]) - if 'creator' in keys: - canvas.setCreator(METADATA["creator"]) - if 'keywords' in keys: - canvas.setKeywords(METADATA["keywords"]) def add_page_number(canvas, doc): ''' - Add footer to each page, drawing the page number + Draw the page number on each page :param canvas: / Automatically filled during pdf compilation :param doc: / Automatically filled during pdf compilation :return: / Automatically filled during pdf compilation @@ -693,7 +787,45 @@ def add_page_number(canvas, doc): canvas.restoreState() -def export_flowables_to_pdf(document, pdf_buffer, flowables): +######################################################################## +# Main part of the script, handling the global formatting of the pdf + +def collect_parts(misp_event, config=None): + ''' + Main part of the PDF creation, it creates a ready-to-compile-as-pdf list of flowables from a MISP Event, calling subfunctions to handle the printing of each element + :param misp_event: a misp event + :return: a list of flowables to compile as pdf + ''' + # List of elements/content we want to add + flowables = [] + # Get the list of available styles + sample_style_sheet = getSampleStyleSheet() + + # Create stuff + title = get_value_link_to_event(misp_event, ["Info", 'info', "None"], sample_style_sheet['Heading1'], config, False) + subtitle = Paragraph("General information", sample_style_sheet['Heading2']) + attributes = Paragraph("Attributes", sample_style_sheet['Heading2']) + + table_event_general = create_flowable_table_from_event(misp_event, config) + table_event_attribute = create_flowable_table_from_attributes(misp_event) + + # If you want to output the full json (as debug), just add next line + # paragraph_2 = Paragraph(str(misp_event.to_json()), sample_style_sheet['Code']) + + # Add all parts to final PDF + flowables.append(title) + flowables.append(subtitle) + flowables.append(table_event_general) + + flowables.append(PageBreak()) + + flowables.append(attributes) + flowables += table_event_attribute + + return flowables + + +def export_flowables_to_pdf(document, misp_event, flowables, config=None): ''' Export function : creates a pdf from a list of flowables, adding page numbers, etc. :param document: A document template @@ -704,15 +836,16 @@ def export_flowables_to_pdf(document, pdf_buffer, flowables): document.build( flowables, - onFirstPage=set_template, # Pagination for first page - onLaterPages=set_template, # Pagination for all other page + # Partial used to set the metadata + onFirstPage=partial(set_template, misp_event=misp_event, config=config), # Pagination for first page + onLaterPages=partial(set_template, misp_event=misp_event, config=config), # Pagination for all other page ) -''' -"EXTERNAL" exposed METHODS. Meant to be used outside of this class. -''' -def convert_event_in_pdf_buffer(misp_event: pymisp.MISPEvent): +######################################################################## +# "EXTERNAL" exposed METHODS. Meant to be used outside of this class. + +def convert_event_in_pdf_buffer(misp_event, config=None): ''' Externally callable function that create a full pdf from a Misp Event :param misp_event: a misp event @@ -730,13 +863,13 @@ def convert_event_in_pdf_buffer(misp_event: pymisp.MISPEvent): bottomMargin=BASE_MARGIN) # Collect already accessible event's parts to be shown - flowables = collect_parts(misp_event) + flowables = collect_parts(misp_event, config) # Export - export_flowables_to_pdf(curr_document, pdf_buffer, flowables) + export_flowables_to_pdf(curr_document, misp_event, flowables, config) pdf_value = pdf_buffer.getvalue() - # TODO : Not sure what to give back ? Buffer ? Buffer.value() ? Base64(buffer.value()) ? ... + # Not sure what to give back ? Buffer ? Buffer.value() ? Base64(buffer.value()) ? ... So far only buffer.value() pdf_buffer.close() return pdf_value @@ -745,12 +878,15 @@ def convert_event_in_pdf_buffer(misp_event: pymisp.MISPEvent): def get_values_from_buffer(pdf_buffer): return pdf_buffer.value() + def get_base64_from_buffer(pdf_buffer): return base64.b64encode(pdf_buffer.value()) + def get_base64_from_value(pdf_value): return base64.b64encode(pdf_value) + def register_to_file(pdf_buffer, file_name): # Used for testing purposes only pdf_buffer.seek(0) @@ -758,6 +894,7 @@ def register_to_file(pdf_buffer, file_name): with open(file_name, 'wb') as f: f.write(pdf_buffer.read()) + def register_value_to_file(pdf_value, file_name): with open(file_name, 'wb') as f: f.write(pdf_value) diff --git a/tests/reportlab_testoutputs/basic_event.pdf b/tests/reportlab_testoutputs/basic_event.pdf deleted file mode 100644 index 18ec6ae..0000000 --- a/tests/reportlab_testoutputs/basic_event.pdf +++ /dev/null @@ -1,391 +0,0 @@ -%PDF-1.4 -%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com -1 0 obj -<< -/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 6 0 R ->> -endobj -2 0 obj -<< -/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font ->> -endobj -3 0 obj -<< -/BaseFont /Times-Roman /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font ->> -endobj -4 0 obj -<< -/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font ->> -endobj -5 0 obj -<< -/Contents 26 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -6 0 obj -<< -/BaseFont /Helvetica-BoldOblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font ->> -endobj -7 0 obj -<< -/Contents 27 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -8 0 obj -<< -/Contents 28 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -9 0 obj -<< -/Contents 29 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -10 0 obj -<< -/Contents 30 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -11 0 obj -<< -/Contents 31 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -12 0 obj -<< -/Contents 32 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -13 0 obj -<< -/Contents 33 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -14 0 obj -<< -/Contents 34 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -15 0 obj -<< -/Contents 35 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -16 0 obj -<< -/Contents 36 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -17 0 obj -<< -/Contents 37 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -18 0 obj -<< -/Contents 38 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -19 0 obj -<< -/Contents 39 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -20 0 obj -<< -/Contents 40 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -21 0 obj -<< -/Contents 41 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -22 0 obj -<< -/Contents 42 0 R /MediaBox [ 0 0 396.8504 612.2835 ] /Parent 25 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -23 0 obj -<< -/PageMode /UseNone /Pages 25 0 R /Type /Catalog ->> -endobj -24 0 obj -<< -/Author (\(anonymous\)) /CreationDate (D:20190222095950-01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20190222095950-01'00') /Producer (ReportLab PDF Library - www.reportlab.com) - /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False ->> -endobj -25 0 obj -<< -/Count 17 /Kids [ 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R 15 0 R - 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R ] /Type /Pages ->> -endobj -26 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1591 ->> -stream -Gb!;d9lJcG&A@C2&D;s\5V.[4,%[F&2JJ(#]#ejM*Z!s.Od/=gChI<,7fWFIAp"u9;'T8T]Ns'GCf@L&0>e+4W)bBbi+SQ5D'[g77egONb+ONT`%`a8MM[S9+:+.F4cQE!=t1Zlhf`[B%M*sg'Vs%h'IJ:#ote"[5tOg2F#Z=9JE=N$$k-br08a-bI.l]SXHO:7KS@R7RG`-.$CsI82+r^0I2_XGOj/+-HmHcs>lBi1J=uM[^a$6tIoBZRh*HaCHjL61IZ)^I@'39bj.=@(-A^\B,19RPq'+!D!#LgCug#MLFq^3i"_Vubeb@Z`KJWj-E-KEG(CTST56gN!2fhr#Ym#n=3*,:'\m@i=/8?_V.P/#H2ghp2e;>XSl"IO1I<>+<$%##KM;Bb(q5mX&_:/so!NImM&D@o;[#;fQjWZ^eUU""4k_&]PGIGqJ(BjdjMI]&!b1h[#<$arjGt'/RG"l[@k0Zqc;Fkc2CrWmb^cEa0);J2)UVjn)Gn1Dp,Zk1cZ$n[uVp:EY"0SleN,iDgnMoXXIIuqa>b5i974W<55IED]\8ArE'[q4eka,fk[_2j^W7e^qd=^i^*Hu%k7-AcfBYmp,&.(56oGkKOkP7m4\\=7ekIKj(V[hnAnGR<$53r+2@Cj-*]tJ=)odLYrD01\*OUP%Uq+2q.dQ?6t0RtuZ)3A\P5iKVo`0A06NfR-P]EGG?0+_1F7DaqP0AVl;7U`QNNDLUe#'jA7e[/TDjW"dREF*5BFa73Rm>]5WPJusKFRi@]MQs`!VHMW<.1j`3lP*Af/Y7k7,W'Y+!d%ui.6Oo)+B"cGO&RLt,LqYI8D,G]86N%oCSY"sEk.U@=-%K1`(foNF)k3]mc'#PH8?6W)l;H^o.@8)6WFqmKREb9;6@nt][p?ZG7dSNOmDK@b?(u)Ve#7rHmL:a9(l<.=0aTZAFH.<,qF%/p85r5Z8/=gn!&2G/$hk9p;g0Q199L(M**(3ZV'XTaA[+7X5kmZ4=)A]9$Ha8Xgj3d\%=fOp~>endstream -endobj -27 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 2120 ->> -stream -Gb"/)gN)%,&:N/3m'Mro2e1:GnWoS,36l*`8Rg:KqW&HKKIk-U-mW>'J):DU;PYnf'((XIes7u=@^-.C$XaJo;]tk0eaG0qGWg_TG8)HXi>AMt"UmW4gYq)fTP@n#8_([YG-o[t#iG?dn+[kN[CNi"@1!8+2].!riCi5/-\eV6)6p0])$e1q2Pl'M4>rD'\[ZG\-6Q!\UN[PY<9V%^5<.6*Lds8'apuF'']Ir-_NlUE@GaM)?ci7+_KB\@URo<,".+8HJ=D$#2oAsn]:()7\@+bL!#FZm-lB]=fKSn[6fI1boV6J5W^:B$LFq%oi*15HK(r0g'j;)2q)KgHI_d;P\20HC/]m4@>l8D_:Pk4hUFQn\o"oS9F8,-jkYXJ]Td/Ig;6$m,a!Y:Pf6-FZ,ClDZ@"<7Q>i663$_stN:1"[QoX#m2WqL^XD+:tXY03Sd89;?hbCdGKKZ9KrEHR.*4t]eE(o9XC'uW5Qfhj#>/Y$5f?j@e-F4Jk:?10CfFGRL%C@Hc:Wlg07tiFX/AauW1JR+R^]O?<91*$OHS5hGE;)>j/F:E67tP;:Rr\+S,=X5F=(gQDTo3lDMq'0q;!QbcO+i?TJo?#Ub8+\".r&Jhe-0_Gf5ht[(e!1A*7UhckK)WGP;FUYCf?iDBL5T1F+o>"iHc?JJo5OpTi-(^leirWBttC0ZQ<:u_4\b!*)$R??f"E;10.T3c"iY>Z;h8'1=#Nm17o;TZ8?IlhgZLiq5#sn9DqMI>LlBG8SamP5Odjj^_SPOH!i_7jLB1.IUIL4be$k9?.Z7!QGAi)4`38;[b@^[]Ig&$<]`/r[cQ#kC+$U/M;E=um8d,Qn'WaLBJ`A#D+*jE7ND/bi105.N@8FoNnZptQplL;f6^>NgD;`WXiNX!$+(:UX,20AD:F9s>8rQU73WXR67QI?!TB>`6Eu/$U*Sqqqm!M*G6gE()[-b8^(8da6!rl7iZL\GYcY:]T0OI&%Fi+decETp,"l%=(m':5#E8MAAeW?E:)5Nb(Q>V7"dr'3#N,#l-*[PYM+/d/K`jjgJ^-Pr/]0)=PVA`j0ImlcR:W9c1Y'>/[7,g2^5hm'%&O=i;C[;I%qd3kLfst97*/=OCW[0-iC,G`-+;nQUe8g$C'j"'..ULV`S"f9X3=KGq&&`l_$MV&E=<$[)-&("^&jf2P`!Nu0+`]e0LUD\O!`.e53-gCNYTl_O>s;Xcf68P/FfEc"S\\ujAmKokK"M&9p-$ec?Y-Vis85riu#>b.>^+Xqb0:)dqPOqZn\imV+*q)8Maiu@I$l.BM$cZT%F1,G(SD=_Z\3J-q<-<8j494"E+7O=ScR4`O/+]A[O;!B.a"e;*2`(133H+5,K\7CJL,jl',\lRESZR2>%1HAXS)9R%\7\($]=$doA:.Dt58Y7+EXhmXuiNNX;R\UZ56Q:ebYoCm=A@=0"mocVS/+nCQ;K"C%>8>bu7QT/dYjGEE)Q-aD[[h/;m1aB`r4u+Ps2[g'Gtt--HRj5^^J\]4m&JQ@Vh\H!=:]OlmY(:'@_M13pngf5*%-d<7/FM"hH+tm_R!q>U%7=U[sWt)nePcCKCTfE#rT3EqE06Ph(t9nEN,5N'e'n_RDiaKmBKsd6s@2,hLf%+0!Ec6gMKilqngC8f6%&iU),2TT0Afp5!(uWIt%=[h1q&0^V%(d-rk7-LT8"sfYWh:V\RT]Q!E<$Pt,GF5cu1'eZ-R=L`b(Xc^X+/7G=DF5_Et>YeG2NmH#-*"S+8>,?4@jCLO+GaX<42="nT=ap$X"aSb@,3&Tj`SEDsJ#AJ%4anGW8U;@@9dL+E1B3`8Uub66_O;+;>sendstream -endobj -28 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1271 ->> -stream -Gb"/gh/AcR&A[3%=,_7$PcM`grIfUCV9*"i:$#VNqn`^_91$gWMok)Od_Gb3FtcbGfZ&q$2dg..41dR9_p&#](ejnEoA:YopqqYA(Dnh,-N_L'"6TcV4/6_h\V'r@k7[@8YC#fg&iR%pc#!e),*#\0'3ocmu\MfB.i?AmQ8VIra5PRGE(.`!5S'FLSd,:%tWfaN>B5#&o=8\20oABqZXLK"Jec)UN;:Pnj_F5KHC^uDtn-1n?`=8psJ,T?)dWMbbW-n(Ajgpdu:ukR2FPY8_(?Q-+f0-`!$C-mK<&'C(b]V83Rj<9Q$9diQ,tYMfPD_O0ob]h8:X_FYld@<+qap+6?F>/Lq`"p-0'Ue]Nd!RerR@"4pClq7;l-k_ViNj&%8DhYhs9mW#Vi"`$P95B"-A%_rZ!)>_a>/sBcUB8I8"U.C%/HZET0;;g!!.Biu4VIpX]#NN^#i90J7*3q^(jajZ1fhUXpInA;bVID("9sd[D+KM__3@G9f:CQQ3YIq(*iA3VDJ&U1=/t#fG[-]lQEN6V;SIdoj&p5gQ7<7VUainGZ?Vg]'eXKVb4R_k]2u/\$GY#t6J[)jNuc4[)mR\+%Sj)+E-dC9lr6B]/>`$W-i4BnQrWb!7\%`pS"1m1/0pj)h,jcon^r&TjAhV+,kNX6[nAb-FjpZW06p_YY?^i!kF5&A6Ne^gf7?t\RbJs>DCoSQ_AKI27U$_)29&/p@mh<"Vp=)+Q\4Z)=Ld@_$Q'JV@9O2jMfC?'Q;gX";aj'/bem&Y'f_'jKA^aJ^b@@Cmbt&^GK^r2TfC4lE']=:`aEiZK4-`L$IaT-"FZc=+\->KB:gnF'f7@IJ;XIc?HUc]F7=FnY>LrEh^p_')%LaEehdBendstream -endobj -29 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1144 ->> -stream -Gb"/i?#S1G'Sc)J.uqQm5`5)Ei`u%+&4-ddDAY]XiiI]Sl.U6Us.7\UC"&&p-:D>/d^9\JEI#ZqO.HF0Sk9O,%9s^?4ED4-]`4S1lq.lqfnH5:SEU\>%n"r&[B'%_$#eZ@i5,o_39$)A"9F[bpd)8hu].].n2PA4%+-s.L%ihB4oLMN`$jMnqa"a!1+"Z=br\0,k.b9-/dKI4[^f-h5f0#XME4:_)ZJi95m:lrcAR'kMT5j7t0hf7?dgnl73YbCAttAQD\mqIipie3jl$EH5d&R^Y\hTL"Lc,hkn-YaX9?>._')ZjParBuS3HSstS=8V_S.d\2=EJ!8.o1L?=:fa1^B;^aTGkNgW.U[Q["]hHknI(]n*p"f+k"8`%n)9I",I2ShDiGTYUiI[ZJERR)T1aT84io1('-7M;?0oi!7NG%YZ1g>q**&A+'5rsH>qi'^1udQ\\bVqZ&=VRG7EjQ,^Oo!U$eWjbtOggFJ<6VUs"kd$:AGea!@)=J*9X6^f[DOmM;QFYLl"#CC]Zk7?gaTXnFYpcYeC=mg7haHlms0+k+^)0CKlutgd5Y?nL&f]GU)Qs;$LGl7c\LZC@YiQ(rBk>X3*W9/DFR@JZ$X7KNcK;Q?F7?g5s(;GZD_cXFhZ~>endstream -endobj -30 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1266 ->> -stream -Gb"/iD/\/e&BE]*;\7S#YCK^rmmrQo]8$k8L10?%^#tTb'QLd_DM*?6;0HJARH<(i4#DY]-oQ>rO+3UB57.uVOagd12_5jDDS]Yn"pU5B.L*hJ/Uh7q/nXQb)iJ5L2AZ*bT53Co#f@)iAWQq3X?Wu/P0MOdD"3N;?IXrfnh*S]#+Y\Oe$U0<:XV^2uNGpG4GTp358lD,QQKC24km1j4B^0P/%4kjl4!Va^>@*ol23"K='33SfBgaX1L;PSfBeu<]*WNBRf1Y#NK,0K)M@8)MbWl`_"Q#[B!Zo@7*2PXg4e'HeZ8B>p<%ki-h\#5_=)+$Lr^VV-Us)OP>^i9FP)e_,8glLoZS[r3@SWt34r?Fdh_qs0N*/>ha6:T=#t7iL&8dNg38>%t&!1>T$9ANfXhZGuVFb2+D1"n>B-.!^l*d\H/Ws*$LeH_8TZ%t?U=L,3)K/0>U>T:`B7O.pBC?RI9pY"LI7#1l\`;d#ks![LWphubd^B?FaJ)^J?r"#J6"ecZb%l*0A5#2$Jpcn!WU!g1[s9cc$sLeIV@a@`;daQa&Go%19/4'ORN2ud)>rsaFB`Ts!@4XN!nj#:l#gcPTopNcYmikJW:muTtO!a[mh'4;:ImaOd/UXp]$aD,msA(nS[@d1Z:bit^DoEF[eKg`dJV$><$$:ok-1c`$s5M:24juTo(9T-"0;gF']aZn>YLiaK]WQUNd?`L>_!jb.+&?O*L!YO`7NHoefh[IQaendstream -endobj -31 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1235 ->> -stream -Gb"/igQ(#H&;KZP'Ei`g%3UDC)4iJ06])XJd7p1go=.N!c8HUh`P-b7n&gel>HqH9M[em&Chc@urh3pf8N&8"T'Ln$Ef3t$A,mTNhq3=m0(cVhrg(g$XZ!np_t7/+_82d4VFYb.+fkJX\'g?>pA*n=ea-T/7@Kpu>R>MR"(\m/Q"Mo@ZnRfukFW9U&4`:ekW;Lc!o2m/XS9DGdRS"5(5'-^(YNBt@F_s4CbS(P^XmdJq$YM6LfK/jDh#iP%,pQ!;kqHpE[np9ib0/(lL4C)i]!QW_>EWh`OCukNYYC`3!5JOr"NLR]cM%D`lq.6*lPM*=2Cie+X*":-.W'Lmsn84smNB;Oq3;87/g\N#.S+sM8dg%ADgNh>.&-hk(h[N'Q2%K/qJ^R]@JBfCu$>WiqUm+"lfDA;uGcah#V$ak-%roirgA>lWe!*]XE$t?kUXB/csW'Vce2RR9OpVh/uq^-\qB(h4;]b@nq7+)O`B=a+*[BoX%f+B6'W*%35i9:Qu7WVH^!=ikcT%C/qraD<[,MW4akoCk.50=9%ZHjOo*U?%L0$&P`Kd*$"8V/?(F[F7bU`s[kMXUL!?esMoD<,ThpWM*Nq6O#fn,DbO`[@)Cop\a&n@:n)\Po:q:0%hPDAJ0.DE0-"1@J;qq`[@"X6cl0rX#L&H@tp$6/+uH@RQ$?*&n1c@]5qX.:UjTTuWs*U\Uh1i=h2nPIF"o*C29k?E95Kg:g:W,11t-rXkrOqr$R&.i4o1Frt/f%J)gP0r"$>78)!4QK+AtkMiIars,I'AiP3l>,"7,e7\k>A=;O[U04Fr\1ao!#,#8eqar2DGAEu+e7^M'g'Iq&#A8M0n.f>W6T7R%>;gm[3S8.t?r',`37VU84ER["2TVFiQn\=V/nc(`LpXp9X2NtO0Q4ura!WbZ2-VP/Qk`W$`,XQ[rAqNW\7VN=JE!JY^S/_LC!rdgqBAc"&RK(:0rD;O(j8Ut9'?5@sDh[M)p8T?:Z15U!^[_=dr.boK2bIXc]Yl(2F`s0>'L_/ebRbpZY@Aj\BQU5F(haHQN*mendstream -endobj -32 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1186 ->> -stream -Gb"/hbAJ7X'Sc@.$3K%^!G92iiGFuh;R@k+-;h?%3M`-0Z5Z;/iO:HdHShKcOtAtX`1X9N3@'+QkCEJB+:th@@Jq&[6MrMNcX28('XK?@>8FR&`Qtm4LWMT/!ouCopZK_&\=^XHQE7A;!*WPHM:k3hc1b]@?Hf"C3km/noY",1?jK?&eIb>'nrqY!@0PZ^n*>nRAJo&UOOC7oj?s(qY*6rQ-EYbF=;INYh[`V&kFf^bXA;Y:)@]%/3$k5'8ogK!d\X.fG1MF*8tjk)q_2_]gqE,F%36?h(#o$^&p?-h=j97^.Ruq1bhJ!=O+*KLY-)^?=uG()Fm6ob!t"A+e@kYPpO9(M,N\/srcWbP440o#]2*.fr'I2Y_4>$E>IJ:NG-tGJbN7Yu/%jbBYC2)1f?"grm3ar&M=_+?(q-G%]DX[*r+sFIqrrfqnFQ/lO%LKJ"0PY%fT@a8Za',2TAPV*C^'S1JV/FD%Qi'0NCkP^3O1DD;^)>%;FlnTLW%#sPVR@+W(`M),\5%M9@s3VWKT4].dn7TlRtFC?eqK8qo=t(Ng*"3s&S"F-S=ZT0VVRSHAGU[f=t!_UVqZ\?G%+uY0rbs2uS1e$C"R5.!t5-:g/:?bWZAuu:tI)Z".0FnpIK+'IR%Oifad3;**3G2&@5Zql1Ce36AcPAQW<:lIgR,j"n`^@`C*NMZ./Zg)1h['8(Qo"6h4Nr;^](3W,rt5/fg2Os$EoWE`ALE_Ro)BZgs-oM$5HgiS=8[Q-pc$J!\Y.=%/u"EJBO$KW;AY]VV_"#j2C0"F6BFAXZFR:Slj!GdJZj:n:BCKojVdQ++3$NY=7KYfrs8P/=12O+Wc?=L,8X%mJ&QoVs&nj+\t?GYVqctK&Y/"6"[U(76J#dL&Td4MG>[N/QDMi6Ue\4#,]&Vt"Vn9$rs@U*R*L53pFuR]Y'%86^Z,8Irj>P`mN4PJAbX#q$#RY2`IGl,=n#cf!Tmc,A$Ql5A=4R=@$OVm8mdlhY[,:0>-!)D5%1l<9=M.!5D:(W`W$Ue5O*gB-f]^O;s6Dp@e)`Y**2&~>endstream -endobj -33 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1340 ->> -stream -Gb"/g?#SIU'Sc)T.gYJg.j1.5LS)35\olXZFN`qb];8%JEGHr(j.sc20C;/neB,!JW9YEO?ab&%^-4XnTD&Ho9AcuLlPG6P1aUIT!Wc&n"T^mmRJoL$R8,q@2A&]7D'JoJHo_Xr6PsRb8:q%qiqa[`0$'IVSkS%(l*L'%k@p=SC*8oLAL,7$#rPrq$09:]a."o(]r>4-g)`jQoh[uVh8IuT8q:bNCbrK`P-Tng;H7e1-:P)8qrh87TQKo0,F(mb[o\&3]pj4VKWYuruotFj["SMKo\dfk0e7K3g'M:HTbu"EiHP4]QNi#>!@r(mX1Z`+4ZMaRck<8b?=7X$is?"_&Aq-:uGo2d"%h4\O:A#;I6.n4+K2sr+p#_+Laer:T*13;tUM@d=df1\]pE[@H?fX%9rlfXVd+6>5&.rt589i'4bFPDQ5rbiLTUn#RN!5N712ruh+2\b=5`TOm,tErap%["ukFA2,F`jgI_R5]^_8#a"6#1$o>0`7=p]/U5mR39's;n;g0MJ:G9Ts4eTQIr0Ks=g`U24nrsi%,FP.mMWU$Ge7V`b4o'#)r\OG4rXHTc]hl_p&)XN6.Q&1BnLscU_*d0g*q+8Y`RUmPja6"bC3DO?FlL0&ZPa&K`uCKa]6Ao"*;$Kk6\(dNmpfrOZ>d/8!e2j>oC]BGRLQL.dgJNcOoRY^qZ/Nr7[ktoujqF#1]uQ3c@icX!-HHjUgq$rU:#Fs7>+Gd#RsUi-f(JHOM7K>)%W?69CB6D.FuHdu9('EjPXM6r1F=QbAj*rrh4rm:"\\PDoG"0;F"="1am]nojj3g#"i.42d[QjM'48PRPFRV<_=8-Y3fdZ(-b.'e9P='[MD1dP1&3((=\@OmAt=BH.6EqS.l,!7V)f#I3/G#dr%lUN%-16aq%`dUn]">XIS3;OK4DNQ,B`%StP5/:r7!^-N!2B^:labG-f&jkb@B#$=iTgl?jBeGKB9Hj5TrO/i~>endstream -endobj -34 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1447 ->> -stream -Gb"/i>BALf'Z],,'Kab[RMgrFBCC-3-0_eckq+10fA5!"-39<^CtIRY$au/XJ,i#U)io@IUa,9!^Wo..-S7Ssr.XO5?P)eqPY>Bc!@6-a!)GVCr_-Wm7t6N,&L9WN&U[\&f>>E:3n6n3bm6SW2Qt2D6,r&&14;rq0/adL*eu2R7[epXqnL1N:#J4EN9JM/C,6'L'Ej>7ESQM3hnK'Zq_GiV1\ODKJO6q"6JYgAV(0toa268dC$-+G%8XKS+Yc!1KM.V0bQ;o*%[L6kVGFefPW-eL>>p\(L_GtkJYAq]Cg!ri_N2qM-GY'fl-O?`\Tn/jtdDJ#3IS%^0).8P@%8l00&"]1Wk_jE:%BF%MYndUVMa]W3j)BF&,c5B9?c)'OGjI!Cr0.+;rID$DnG992clqaX6;YZ23R*Vp!Zrd@X.J94..+HPd4$13[\S20W]7F!Aq\"B]'Hbp[-^3<>g[o*d\m?2r_Tl)p>YO_A4A:b^J!YN8T);1UWL88:.MQMp9\LtjLOu`HlgkMDm!AG<8W&s7DZ=uR0$VM-Z9d%CEN'Ab-r(Cb63baoF7":^(N\4l*8)`!M%i!$q+pO>+dO"VNDu!6K\IBibqE""(6)k_&a]k)"XWFZPr`!$nC[(Ed-HpJD3$@K]\&4<;OBJ>t`=j1?hUhYN$MQa+Bs%(i?cpBQD^F]W_d5p;%tu5*rE*D-L"--a\2G)VOFF;6I=NKZlb01oS2`VDq(9OYW!.P"8SjDub[j-ERLl*T_,J^;Z2M_EGTA[MPl2_mSEH"/q#B1Nqi<4+]3TCDV8g_gjQZ-BN!"K)j4ATouA;BUTIpq-:\^,dMpS\0/P%mQOKh)\WE/QHe""ub=?9k;Ln:EjL$\14\HjheQ-h)_,(jSFEOD+8*EnFWr$o9!OAEMq0!/=qEj*s,]d9H0+SbYMjAj2g(b_NaDXh[.38Bc&r@8I9$F'IU&+SgUCJTncBqJ8i=D_4Jn+Mk!P\3cOLG\2cI!;$CfkgUOI_LrEdiJcLu*Ets.P]2)XBVU5?,%0gQ7j#*m5WK;3K[W8DS5RP2:4Z/Q):-"IdX#[2k#u6LhD(Ao[YsZ>H9ruMk\E*9["`PhHLpAsmpu&$Vi%1<~>endstream -endobj -35 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1439 ->> -stream -Gb"/igN)%,&;KZH'U\-ERi,*(G&bZ1%nW[i:0TYLVRJP4<[(\'/B@;8(QqIS9I3kOfqe=B8h:4-?%#eft-g$C8WW.C!IE3+@=5@1!5^[;/RXZ)Th1"bDXbJWl67b+`frcOGj3C7T)>fjdI\6N-tVQ?R_8p;i,B%,8DXPDT5ZsB6qboTm[d>S8;Qe>#g-4X[=,D"JXM-W-'%MF"UF4QeDo0m#@Rl`:?T4:#kBSe5$OK/Dm&@HdDA;rMNk#F*hM8E7&V6,"D"6JOkDJc3+h(UJm%#lO-[O?9)rB$XLV\\M15qG@E5.X(WH\_pQ?C_>^kcX.#qie%_^QgJNtd4VZ!AX_U9:.F."iJ1Q4q:(0/FPCJ&l.e7mJh*D#$1<.Mu9n:P#cMoG7)(N.]N3*b.*5C:o7C*!"ukj]cT&8TWT8,s1?mc&fA+6dhp%+`3JKBGdBtr0P'H*.$Os_\;jk&TD_@0:;^drr=]o;/n$Hn,i$ErcV';*Ba:9?I2tGZk$KiV[M^K(Ne#\(S%e!jW$$43Zc0H>stDG?8u4=?@5qqj])pnRm-8CVu*?aZ2qeV.PS46pO<;MkTmJt!!AaoZErfG]/F+PS\"e.0Z1Vb^Kdnt?U#WnDf9RcO4348okLb;ZeR4rj'k'-j5)hH>\;6J#tBV85W'U5A1O6/m5Gc(W!6S,$85ol04]bQfJ$]EJ-L2toEceuQl^d257Ag-la0G(cb?4JoFsIN*l/Zlb7PWj%1bpfA,~>endstream -endobj -36 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1466 ->> -stream -Gb"/hD0$US&BE]".5]L4DJ&=`8unl&CX]*.:8b3-P3H1B-eTb-,_5:WgK/od]5up>2l)9N0K-XL:m57^%gY2f)LlKR%+3T8-\P)i7[\Wk@pIWm>U2#39*C\R0-9Or:b4VWR=?*^WnHXPB<(SKF=1UaplH0-*okrZX0f&6360HleP!3^Bf(0;7e4$4*Uh@bnF@dSDI0+[[Fr],;c5@(X"X_lK*u+TBkgC[q)^:C]>+sH9$pfNl"qZ=sSl;jFWo8R#]3qZ7,=929i0We-c9knl=MmXF8\bE913)rBectfpf%a?ScUj,UZ!m'T#L76SqS@riFVYSOm340b!;@M&qjbSq/bE6(RH/JKoc\[2q],)ILM]F2/Ts(C!J)9YOgt^uB_qERf0jWF/m\f)o*0*AcBZ9GpM&kEtL\!\8TL'e,h/Abp8!jk]@Xe**3-[Xb3@0TPVqTM4Vr.6^*+aFEOpf!OOBdt#^\2[oaA(=qa&T`C7jIT%lut;h`3rEnq5%qte^+Ve^-#V9NQ'$_\@*)6QVqK8$k6lPH4"Uh(,9dgE=j<%g8ja5lRNW\gpn2'RQ%:fKK#qMS"5!GVi#j*G#A2Tp*)>Fr#<56[5!p>5e"!BgoplaUSA=*?K#>+NrMAhBEs]^SI:JOt+i?;M1(!_C!X0:;.Trr=]o;=UQu!3WqikV7FI_fMt_0ARV+-\jJWPHmf[#l_:nCC0g<*BVE&VLW#7Ws90rLV*hd4]'e9$k26K=R=5deahT)kV3~>endstream -endobj -37 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1374 ->> -stream -Gb"/hD/\/e&BE]*;\7TNXFOt*O,n^uLN_<($qob!HV77Iims,o7no:f?eU>fWHB1b27b,r'AK4@jS@,sFhdErYl"Uhf.\T5k6W$@"9P)e&ck9%RJ]W_9L^*nCC;N*eeW^Y4^WoT.253-dNAS#rJOA3j957=AFc$[Sum=2k'?7[S5IYSO[u>u!9;c4Lg%d)&`iQXjk8iAO"ulk<)bW[U];[`8CFgV2-^LqjR`,X6(2^4[+geOX[/KEk6Yu9:L*S>Pl#;8`7@SN]@9^E`i<2=]I7g.g`K"MPM>!L;,)fIY3'tRpO(Rk8%budQVY=:5+gi\A`h:0<.KGa*bW[Yu@.G,4/BP=V8>6;NQCGGms.N8ZnqN]U:l0?iUio+-IC'H>.UX>Qc?N8d^1Uam+q3;M$ej-AsJo.#jP/)8ST]dr*cDi3qOODsrD%c26/Mp3Z1Zg@54@?s^tr8jN&H_C1asUZ@E(I;DL>)"3ehHTj\%BPFh/LaiX&8E6EtXZ*41=a$b"%\7itJ@W#0K*-i5Mu7?VWg9R6e:6QBgCY/M5;]&Hs5Z/jI\Ja[1'TG)fh*V)02c9@)meW+X`bF+h)2R?BCHr[qZ"\?YA]JVq^gQ0X/[ZpFRB">rT"hN:k?l<)q,@s&__\6h;>u+uC6/BQ.g.q[9sANS_jYhqBUom!Q3lQil-CI>[FoXEV\_YEk:WR<)4Y4s%3\]`I4)kJA(O?&VpGu:U@Cl`OfEs@!fll)aM^pV1+FtD=dAZS#,e)I5Q2GJ$*kK6*]&=%Ii="3*L$0@AA&&S!+%YVb3Pr9ghoSo[l3tQTJ7;Pg8i`Qeq#ie`K4"jqcLV=XR&K[C%cm/PAam?p^7H>>_rLg7%cf=*_]IlHHt`rH3%LN,R2i:GaC-3HVb.FpW;+2%WR*#F^+=nh`'lMCV<"`]VkY8_i<]JdE'+1Ui#YAtEpZ@*`K-_fn\A!(=/$6]5iG&PkR^*$MB>Wlq!fql@46SPVo=^l"i":>OBNc:6h03a'gKe''@Rg*g]~>endstream -endobj -38 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1469 ->> -stream -Gb"/h?$"^h'Sc)P'g'k`2_u(np?ZG[dPg?se*44gan0bkAT>G[`?Co_P"]P4&NlUhNQA[FutMNMK0fErS!C5#5g*5u>X-:c'F",g/`9=/0"V`:CZ]m'@eqf<@s<@aoF=$=9XlRe,j?b%&=8/$`'m[5jS=3B(moFQS8D"Lkj(Q'g7_tOW=';#D8OY4;g`Q(rhM)0Lp.CP/@YRLO%N8Gs4$'Ygstb?keEIg7kl<6Ro'9>j5)[,;?b1?pN!B!!`6Q0JG-R"UfP;M4L#9C?4EXc1E4A@N.(oJ)3=JDkIBGl-qZ0$ipDf^s9,0lf,-G8@4Y=8.&oa)n\g3iqFX[itBA<=Mh;i'HhMY>"`3*=#aN:_rNe7W^&urMm.?#j?S:-BS)6OO^$sN\fV-rFGqk`$0*=[q%4l&TLcE>\nDY>@ogX3aq]kmd=;SL)^"a8*6@_Yb5dD%OMgKmW";C6o'ef`@0ZpW]<(5VkIf,E`XW:SfPa]sWVj-klh+Fl]D6s1iDL&`@m_#)3bS81_5;o4X;#~>endstream -endobj -39 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1390 ->> -stream -Gb"/hgN)%,&;KZH'U\-ERi,*((5kQJH:U*aF?3-rA9nUpTWLVf&rF&t[gtlr,3_Y*`%Zs-P-"hr,SiVKbPaD.KZ65WIV*XfI\)YkF%-uoPFO=h$8VF@4I_;dO%Kd-N0Y6.G`F!Zp1qqoTgdH0L?+Fl49$AKh2Ym_n-188?@nW[eXm31%5.=mJBpS3t:g]-#dC4!fUK-!5^U,RM!9c&DXBHVVZh0O*"Z0//[;UW"=$C_qOASF.NGCH#+rR`Q73'I.S6&LDp7b<..pR>#0A:,hh,&CR2Q/q%FUJrDIWMXE::KD;Va?QMP2NQ>Xn;BF;K*DoZ=)*H,!Tl=QdQ%=24dhb_:dgIkKs1JD-D"BPHti\V,UeM/Y'm%\u]bD?Jua:oE"'0T/SW'U#j7--H7QIAh_2HZIY?KhrtSsfBkPB(O7d8!lRl"VJ3:e7l?Fd8f'4%BH+5D?E@pd2EM?C%gnF+HYh*(NS((pAPn&=-s6/&=3s+`pBpa8p%kqHZ(p#0D3-)CKVWk-^ac2:T!lqphoG27logU+"m#SrUiM2`L!oFEt8D(58;G@Sj0-5+kpAi9XO<0$heM:Y93!P(n*L%m0ZGH1h%U^qh^7O6"Vf\^j8>?F[KQq_)d??\pRpf2m@;qo\b@Q9!F;3qGpQn^-*R<-Y(T0\R]KA@:gn-J6:7'EO'7Y(ZcOQKeW5iXmPN;'$p+b54f:St0#gD/;@(L)<:*8.;;)!M)_'\kK:%0MM@ZS\RBGLQmOF0IC2]9_m^+bs1])FZ]q[cGe%$<8;S[ZCQ+Uk"a3C'/At"d_T>"#)n=3UDh*XWu-EM&]:ODu_nK%3k?EKZ'SCM,8eqaPfMsO-CXH*ZkbLGcu_.hG\#k`],hJ>7hBb/TMZ/49<>E!![%S!+=^q"+sII+*$$43,O+d"%-[:"9p;L@_p_'%,i=:3,O-B5;ka?">QE)5JGOiO22V3j38+Me)n\\f_eRI#)3%E!XEOjR"L7VlTFV0HWh8ZHM;YdfN.I6A?=Y=MaV47,m(G6TR-?(c_66jnL8e"_u=)T3Z-P6d0IC@B70~>endstream -endobj -40 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1399 ->> -stream -Gb"/hhf%7-&BE],S"fIKNjM-E&Z*@4kfqpKXu8V+"^RaH;5SCijD,0tH\hY.\P':++BqDHJ'q0>r^\@XaQ1>*lLQi.Fh(nECCi`V!9K`-jF9S"@bMrMr4]f\?):EH&s.uC#*reOO_#fWM\.g5UZCM+qnjOi%cU:olLlLQc9^9XdWIVUZ$!kHOt*s.%+Z;)"=9>61jg?A5rQKIEV1Uq?_qoC#)HDVUK=dPTP.kVQtFXcfrN)>*j<^9moeDX/jlrVS8l-C)iBXDjp/%0X,UKCn9aje]%a`j[m2_>:UlABA'E'^e<0KhQR&fPGd"M/kcK_9#NpA__KfW.oA'6.';":oQkh5d<5Jcs;6'8m(^nM0Dr!\AXb7#8,@/Bi['39VjSi*5k,N:QT<$-)mT`au>KOIjFM&h4Y53ggW/u^FbF!_\$$b(UYBP2UT^<$K'5DpOd6KDos/o54Kj[AcLnM"gqVYCCuKDta4W;Kk']a5!sb>M#L%77.N%(f2_s*;i&sSG*!@S(0k6G>V1q;82r`E8k&aR:FqV$6+WXR;mgBuJZ6K@erd0+[86GCe@'I8Nrs<)s,NDDh!!_5\rS=%FH?f;4^gR#;kV7V(*3Pr=@$!j3$m"JuF5Si*FEe9LoFZqT!Q3k_nbV,u0!F&h\Fafk4l+Y?WX5KrVSph!gil`DFL?UK?Xf@c/^H_6^6h.F036&"#.'0ipY\,75#'a,%Qh'A:5^V6;Xb:gO,t%fnRB-"J'5;'8E!F)PUS*7Dh\I$J[YHPmgj&]d7&9b,UQrSTpN"$(Ie6VP0?nH,Z5U37C]I+DWD5RmlVr"$p.KYI?i9%^up+IJ&7WB;*gWDZbWiE[l0!":4T$m9:*mgfih;?"Z#$Q_E^ec~>endstream -endobj -41 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1437 ->> -stream -Gb"/hD3N[3&BE]".5]L4f?Dl\[B$4Cg#t",4^4J*RDu4$(H_q^!h&-6'6'*3%s"_\2T.e/T3i%F;D0O;8sN&0Q=YS_*a/;TO;jlb!rMooZ'2UX410E5Wn'c2_`X(Hi!HZ@-bs#Zpj-](.9.(OZ.dJdB4a<_*sT8`&'p#;gCl>EKl?`!R99un=(;g&+>LE"is'\sqXq$V=2E.M[WS\;d0:*V3]S\1okXN8=/^DXOH.@h$D/ag9Lbk7R<.(o'NE1CLqGFg8@$En<+S-^7EhUa9)Zj`j4.sp^QI9Xn!@6=5=4drJ!P)'j!N!HQi"aHd@s?m>]rslE\_[9DDn'*S)m[U_n9=r[>ImE[(V@b2U:nuA/Y:!?Ut,K#'N(Om*4`S"62KZ5fe6CAW20I7EuHi:r3$\Y)O?o>XKgf$lO8qpds4RU)&j?7t+gSe$U#GB`lMGlD]fS1WPBpTs+g*)GS`Mo"<#Oc/qk&$)nY&0u2uS@BEEA]+lf_HeaR:Pbe;iK"j\ei_K;r[n)`*TX$E6aL%WlG7M[?MuY#[:FJ]opfpC7VYZM?WDf]),dLjUafJ&UfM'"L4QdfjL/YkXi^VQ")JFZI[mSP?g\+%d^0%/I1mtLk)D8%Z_"]LQHT*6ppc:L4(\0K_T!0+4HrBqlp;u00rEcjZlM+,]3!d[*#aeaiM+D*U9Eu,#(\g$/OW4m>J%GO!bQSdAA=Ug:I>]qFh\8Ub[C$$lfEEPO`'TWM?6SmV(1]c^1tl.MGQFIN;j*Z7"\KCECc!RSU]\\^+;O5+F#PJ]bQWtjmKg5nZ(.J+B)A-4:A(+HIr^o]mZO@#mEOon>ThoS1U09_#bW[ERn*"F(rccKg14_6NZ+pUQ6-\n95cBKM*Z30'(^0@B!5ndtq8O#.kVka&c&VsW!OY_=TKQgSn0'#JZ/>J(P+*QNN#;E9O8nJ,a2rqjh#&i-[=s)$iuBsA;APdQ8G6t]DUCtWrD:7^d4\q8'+&ZON(D"5`%j1rfDQ?%;d@1i>sK$ui[#0R;\i77F@E2t_+JTS\eh2I`MOtbdQBq%.IP<0EE09N7++E?FAG)a-&cp!r89b"10j&T$?(XFW">3PJEcfAWCh7f6ta]7*7tp/b@#ru-Y<`g~>endstream -endobj -42 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1049 ->> -stream -Gb"/%gN)%,&:N/3lqCbccOe$(EV3PbfU6>4S^q@-IH:X>Pt72kdrt]aKd9SB?&H'-:""g\64,]j*ZaU1HOtFHj5u^^!8\t4T-L"ZU]coU6LlXs-'Q(k)aj\s%3hD+_^n!*d#?$q97XPN"j$hCBRYWq'-nskX8`HFlH9?DBLi[_@i6I,JNpqt"0X?3OZA5?XS?&grn*ZgqGUCrB%^JOiuKH?g9>h?<\SYSujFGlmHqlhMaRo^qEqg"de#e.>jYj3ED*CD#<7=d)0/R\G6__5rooL<-Q5M8MdCf?$FEIU:)XOq!QWg*CtApL22[ZSdfNiYiA,YdF2-p5g,mO^4)>)\P8`0>&5rg[$gEk-MOamJ*bjm'\ZVcW*=,jWu]O.-Je>@Sg?2-6)@eU-?r0:Xbr3J'7I[7jkj:hrtGpt%C>@pJDP\(eBi=raB;27Y1recQjSI8Ol0WK]NUfp##eGs^lC4OZI>!NqT5hM/6&4SOmg[feK]s!K44G%"'0,k,DoNK/jqKr5ML1*apf6W*YOSukBR#,JZdh`r-4/RoW__B`\46XsDUaG2cFKUS.Jd:ULZg9Q(@8)kERUJ1g;4RS`q:0UKLGTtlDeFKZTK7T9hpa@hMYgaA"rc$)O5Ll=n2endstream -endobj -xref -0 43 -0000000000 65535 f -0000000073 00000 n -0000000134 00000 n -0000000241 00000 n -0000000350 00000 n -0000000462 00000 n -0000000667 00000 n -0000000786 00000 n -0000000991 00000 n -0000001196 00000 n -0000001401 00000 n -0000001607 00000 n -0000001813 00000 n -0000002019 00000 n -0000002225 00000 n -0000002431 00000 n -0000002637 00000 n -0000002843 00000 n -0000003049 00000 n -0000003255 00000 n -0000003461 00000 n -0000003667 00000 n -0000003873 00000 n -0000004079 00000 n -0000004149 00000 n -0000004433 00000 n -0000004606 00000 n -0000006289 00000 n -0000008501 00000 n -0000009864 00000 n -0000011100 00000 n -0000012458 00000 n -0000013785 00000 n -0000015063 00000 n -0000016495 00000 n -0000018034 00000 n -0000019565 00000 n -0000021123 00000 n -0000022589 00000 n -0000024150 00000 n -0000025632 00000 n -0000027123 00000 n -0000028652 00000 n -trailer -<< -/ID -[<0f16994cff7b04926980333b4fec6185><0f16994cff7b04926980333b4fec6185>] -% ReportLab generated PDF document -- digest (http://www.reportlab.com) - -/Info 24 0 R -/Root 23 0 R -/Size 43 ->> -startxref -29793 -%%EOF diff --git a/tests/test_reportlab.py b/tests/test_reportlab.py index 74925e4..1d9c8bc 100644 --- a/tests/test_reportlab.py +++ b/tests/test_reportlab.py @@ -6,17 +6,24 @@ from pymisp import MISPEvent from pymisp.tools import reportlab_generator -import os import sys +import os +import time + +manual_testing = True class TestMISPEvent(unittest.TestCase): def setUp(self): self.maxDiff = None self.mispevent = MISPEvent() - self.test_folder = "tests/reportlab_testfiles/" #tests/ - self.test_batch_folder = "tests/OSINT_output/" - self.storage_folder = "tests/reportlab_testoutputs/" + if not manual_testing : + self.root = "tests/" + else : + self.root = "" + self.test_folder = self.root + "reportlab_testfiles/" + self.test_batch_folder = self.root + "OSINT_output/" + self.storage_folder = self.root + "reportlab_testoutputs/" def init_event(self): self.mispevent.info = 'This is a test' @@ -28,39 +35,120 @@ class TestMISPEvent(unittest.TestCase): def check_python_2(self): if sys.version_info.major < 3: # we want Python2 test to pass - assert(True) + return True def test_basic_event(self): - self.check_python_2() - self.init_event() - reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), self.storage_folder + "basic_event.pdf") + if self.check_python_2(): + self.assertTrue(True) + else: + self.init_event() + reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), + self.storage_folder + "basic_event.pdf") def test_event(self): - self.check_python_2() - self.init_event() - self.mispevent.load_file(self.test_folder + 'to_delete1.json') - reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), - self.storage_folder + "basic_event.pdf") + if self.check_python_2(): + self.assertTrue(True) + else: + self.init_event() + self.mispevent.load_file(self.test_folder + 'to_delete1.json') + reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), + self.storage_folder + "basic_event.pdf") def test_HTML_json(self): - self.check_python_2() - self.init_event() - self.mispevent.load_file(self.test_folder + '56e12e66-f01c-41be-afea-4d9a950d210f.json') - reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), - self.storage_folder + "HTML.pdf") + if self.check_python_2(): + self.assertTrue(True) + else: + self.init_event() + self.mispevent.load_file(self.test_folder + '56e12e66-f01c-41be-afea-4d9a950d210f.json') + reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), + self.storage_folder + "HTML.pdf") def test_long_json(self): - self.check_python_2() - self.init_event() - self.mispevent.load_file(self.test_folder + '57153590-f73c-49fa-be4b-4737950d210f.json') - reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), - self.storage_folder + "Very_long.pdf") - # Issue report : "We are not smart enough" : https://pairlist2.pair.net/pipermail/reportlab-users/2010-May/009529.html - # Not nice but working solution exposed ther e: https://pairlist2.pair.net/pipermail/reportlab-users/2016-March/011525.html + if self.check_python_2(): + self.assertTrue(True) + else: + self.init_event() + self.mispevent.load_file(self.test_folder + '57153590-f73c-49fa-be4b-4737950d210f.json') + reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), + self.storage_folder + "long.pdf") + # Issue report : "We are not smart enough" : https://pairlist2.pair.net/pipermail/reportlab-users/2010-May/009529.html + # Not nice but working solution exposed ther e: https://pairlist2.pair.net/pipermail/reportlab-users/2016-March/011525.html def test_very_long_json(self): - self.check_python_2() - self.init_event() - self.mispevent.load_file(self.test_folder + '5abf6421-c1b8-477b-a9d2-9c0902de0b81.json') - reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), - self.storage_folder + "super_long.pdf") + if self.check_python_2(): + self.assertTrue(True) + else: + self.init_event() + self.mispevent.load_file(self.test_folder + '5abf6421-c1b8-477b-a9d2-9c0902de0b81.json') + reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), + self.storage_folder + "very_long.pdf") + + def test_full_config_json(self): + if self.check_python_2(): + self.assertTrue(True) + else: + + config = {} + moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata"] + config[moduleconfig[0]] = "http://localhost:8080" + config[moduleconfig[1]] = "My Wonderful CERT" + + self.init_event() + self.mispevent.load_file(self.test_folder + '5abf6421-c1b8-477b-a9d2-9c0902de0b81.json') + reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent, config), + self.storage_folder + "config_complete.pdf") + + def test_partial_0_config_json(self): + if self.check_python_2(): + self.assertTrue(True) + else: + + config = {} + moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata"] + config[moduleconfig[0]] = "http://localhost:8080" + + self.init_event() + self.mispevent.load_file(self.test_folder + '5abf6421-c1b8-477b-a9d2-9c0902de0b81.json') + reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent, config), + self.storage_folder + "config_partial_0.pdf") + + def test_partial_1_config_json(self): + if self.check_python_2(): + self.assertTrue(True) + else: + + config = {} + moduleconfig = ["MISP_base_url_for_dynamic_link", "MISP_name_for_metadata"] + config[moduleconfig[1]] = "My Wonderful CERT" + + self.init_event() + self.mispevent.load_file(self.test_folder + '5abf6421-c1b8-477b-a9d2-9c0902de0b81.json') + reportlab_generator.register_value_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent, config), + self.storage_folder + "config_partial_1.pdf") + + def test_batch_OSINT_events(self): + # Test case ONLY for manual testing. Needs to download a full list of OSINT events ! + + if self.check_python_2(): + self.assertTrue(True) + elif not manual_testing : + self.assertTrue(True) + else: + self.init_event() + + file_nb = str(len(os.listdir(self.test_batch_folder))) + i = 0 + t = time.time() + for curr_file in os.listdir(self.test_batch_folder): + self.mispevent = MISPEvent() + file_path = self.test_batch_folder + curr_file + + print("Current file : " + file_path + " " + str(i) + " over " + file_nb) + i += 1 + + self.mispevent.load_file(file_path) + + reportlab_generator.register_value_to_file( + reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), + self.storage_folder + curr_file + ".pdf") + print("Elapsed time : " + str(time.time() - t))