#!/usr/bin/env python from __future__ import annotations # Standard imports import base64 import logging import pprint from io import BytesIO from pathlib import Path import sys import os import requests 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') # Potentially not installed imports try: from reportlab.pdfgen import canvas # type: ignore from reportlab.pdfbase.pdfmetrics import stringWidth, registerFont # type: ignore from reportlab.pdfbase.ttfonts import TTFont # type: ignore from reportlab.lib import colors # type: ignore from reportlab.lib.pagesizes import A4 # type: ignore from reportlab.platypus import SimpleDocTemplate, Paragraph, PageBreak, Table, TableStyle, Flowable, Image, Indenter # type: ignore from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle # type: ignore from reportlab.lib.units import mm # type: ignore from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT # type: ignore HAS_REPORTLAB = True except ImportError: HAS_REPORTLAB = False ######################################################################## 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): # type: ignore[misc] """ 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/ """ # ---------------------------------------------------------------------- def __init__(self, x=0, y=0, width=40, height=15, text="", color="#ffffff", custom_style=None): Flowable.__init__(self) self.x = x self.y = y self.width = width self.height = height self.text = text self.colour = color if custom_style is not None: self.custom_style = custom_style else: self.custom_style = getSampleStyleSheet()["Normal"] # ---------------------------------------------------------------------- def coord(self, x, y, unit=1): """ http://stackoverflow.com/questions/4726011/wrap-text-in-a-table-reportlab Helper class to help position flowables in Canvas objects """ x, y = x * unit, self.height - y * unit return x, y # ---------------------------------------------------------------------- def __repr__(self): return "Tag(w=" + str(self.width) + ")" # ---------------------------------------------------------------------- def choose_good_text_color(self): # See : http://trendct.org/2016/01/22/how-to-choose-a-label-color-to-contrast-with-background/ r, g, b = colors.HexColor(self.colour).rgb() brightness = r * 299 + g * 587 + b * 114 / 1000 if brightness < 500: # Standard treeshold for human vision : 123 instead of 500 text_color = "#ffffff" # Black else: 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 p = Paragraph(f"{self.text}", style=self.custom_style) string_width = stringWidth(self.text, self.custom_style.fontName, self.custom_style.fontSize) self.width = string_width + ELONGATION self.height = self.custom_style.fontSize self.canv.setFillColor(colors.HexColor(self.colour)) self.canv.roundRect(self.x, self.y + LEFT_INTERNAL_PADDING, self.width, self.height + 2, RADIUS, fill=1) p.wrapOn(self.canv, self.width, self.height) 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", "Activate_textual_description", "Activate_galaxy_description", "Activate_related_events", "Activate_internationalization_fonts", "Custom_fonts_path"] # == Row colors of the table (alternating) == EVEN_COLOR = colors.whitesmoke ODD_COLOR = colors.lightgrey EVEN_COLOR_GALAXY = colors.powderblue ODD_COLOR_GALAXY = colors.lavenderblush # == Lines parameters of the table == LINE_COLOR = colors.lightslategray LINE_THICKNESS = 0.75 # == Columns colors, aligment, fonts, space, size, width, heights == FIRST_COL_FONT_COLOR = colors.HexColor("#333333") # Same as GUI FIRST_COL_FONT = 'Helvetica-Bold' FIRST_COL_ALIGNEMENT = TA_CENTER SECOND_COL_FONT_COLOR = colors.black SECOND_COL_FONT = 'Helvetica' SECOND_COL_ALIGNEMENT = TA_LEFT TEXT_FONT_SIZE = 8 LEADING_SPACE = 7 # Small clusters fonts SMALL_FONT_SIZE = TEXT_FONT_SIZE - 1 SMALL_LEADING_SPACE = LEADING_SPACE SMALL_COL1_ALIGMENT = FIRST_COL_ALIGNEMENT SMALL_COL2_ALIGMENT = TA_JUSTIFY EXPORT_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' COL_WIDTHS = ['25%', '75%'] # colWidths='*' # Not documented but does exist ROW_HEIGHT = 5 * mm # 4.5 * mm (a bit too short to allow vertical align TODO : Fix it) ROW_HEIGHT_FOR_TAGS = 4 * mm # 4.5 * mm (a bit too short to allow vertical align TODO : Fix it) # == Whole document margins and size == PAGESIZE = A4 # (140 * mm, 216 * mm) # width, height BASE_MARGIN = 5 * mm # Create a list here to specify each row separately INDENT_SIZE = 6 * mm # The Indentation of attribute from object, etc. INDENT_SIZE_HEADING = 3 * mm # The Indentation of attribute from object, etc. # == Parameters for error handling for content too long to fit on a page == FRAME_MAX_HEIGHT = 200 * mm # 500 # Ad hoc value for a A4 page FRAME_MAX_WIDTH = 145 * mm - INDENT_SIZE # 356 STR_TOO_LONG_WARNING = "
[Too long to fit on a single page. Cropped]" # == Parameters for error handling for image too big to fit on a page == FRAME_PICTURE_MAX_HEIGHT = 200 * mm # 195 * mm FRAME_PICTURE_MAX_WIDTH = 145 * mm - INDENT_SIZE # 88 * mm # == 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 IMAGE_TYPE = "attachment" # /!\ Not only pictures ! Can be PDF, ... WARNING_MESSAGE_URL = "'https://Please_consider_that_this_may_be_a_harmful_link'" NOT_A_PICTURE_MESSAGE = "This attachment is not recognized as an image. Please access this attachment directly from your MISP instance." GOOD_LINK_COLOR = 'blue' BAD_LINK_COLOR = 'red' # == Parameters for description == LOW_THREAT_COLOR = 'green' MEDIUM_THREAT_COLOR = 'orange' HIGH_THREAT_COLOR = 'red' EXTERNAL_ANALYSIS_PREFIX = "External analysis from an attribute : " DEFAULT_VALUE = "No value specified." # == Parameters for improvement of event's metadata == threat_map = {"0": f" undefined (0)", "3": f" Low (3)", "2": f" Medium (2)", "1": f" High (1)"} analysis_map = {"0": f" Initial (0)", "1": f" Ongoing (1)", "2": f" Completed (2)"} # == Parameters for Sightings == POSITIVE_SIGHT_COLOR = 'green' NEGATIVE_SIGHT_COLOR = 'red' MISC_SIGHT_COLOR = 'orange' # == Parameters for galaxies == DO_SMALL_GALAXIES = True FIRST_LEVEL_GALAXY_WIDTHS = ["15%", "85%"] SECOND_LEVEL_GALAXY_WIDTHS = ["20%", "80%"] CLUSTER_COLORS = [0] # or 1 OFFSET = 1 # == Parameters of published value == RED_COLOR = '#ff0000' GREEN_COLOR = '#008000' YES_ANSWER = f" Yes " NO_ANSWER = f" No " ######################################################################## # "UTILITIES" METHODS. Not meant to be used except for development purposes def get_sample_fonts(): ''' Get fonts available on the current system, usable in pdf generation :return: None. Print on std output the list of available fonts ''' # Create a dummy canvas c = canvas.Canvas("hello.pdf") # Print list of usable fonts pprint.pprint(c.getAvailableFonts()) def get_sample_styles(): ''' Get styles available in reportLab (Paragraph, Heading1, ...) :return: None. Print on std output the list of available styles ''' # Get styles, as for example sample_style_sheet['Heading1'], sample_style_sheet['BodyText'] ... sample_style_sheet = getSampleStyleSheet() # if you want to see all the sample styles, this prints them sample_style_sheet.list() ######################################################################## # 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 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): ''' 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_w) # 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, color_alternation, galaxy_colors) lines_style = lines_style_generator(data, line_alternation) general_style = general_style_generator() # Make the table nicer curr_table.setStyle(TableStyle(general_style + alternate_colors_style + lines_style)) return curr_table def alternate_colors_style_generator(data, color_alternation, galaxy_colors=True): ''' Create a style, applicable on a table that will be built with parameter's data, with alternated background color for each line. Modified from : https://gist.github.com/chadcooper/5798392 :param color_alternation: Allow to control the color scheme. e.g. [0,0,0,1,1,0 ... will produce 3 lines of a color, 2 lines of another, 1 of the first one ... :param data: list of list of items (2D table) to be displayed in the pdf :return: A list of 'BACKGROUND' properties, usable in a TableStyle, with alternated colours ''' data_len = len(data) color_list = [] if color_alternation is None: # For each line, generate a tuple giving to a line a color for each in range(data_len): if each % 2 == 0: bg_color = EVEN_COLOR if not galaxy_colors else EVEN_COLOR_GALAXY else: bg_color = ODD_COLOR if not galaxy_colors else ODD_COLOR_GALAXY color_list.append(('BACKGROUND', (0, each), (-1, each), bg_color)) else: # if data_len > len(color_alternation) : # logger.warning("Line alternation for PDF display isn't correctly set. Looping on given values only.") # For each line, generate a tuple giving to a line a color for each in range(data_len): if color_alternation[each % len(color_alternation)] % 2 == 0: bg_color = EVEN_COLOR if not galaxy_colors else EVEN_COLOR_GALAXY else: bg_color = ODD_COLOR if not galaxy_colors else ODD_COLOR_GALAXY color_list.append(('BACKGROUND', (0, each), (-1, each), bg_color)) return color_list def lines_style_generator(data, line_alternation): ''' Create a style, applicable on a table that will be built with parameter's data, that draw colored lines above and below each line of the table :param line_alternation: Allow to control the color scheme. e.g. [0,0,0,1,1,0 ... will produce with a line up it, 2 lines without, 1 of the first one ... :param data: list of list of items (2D table) to be displayed in the pdf :return: A list of 'LINE****' properties, usable in a TableStyle, that are drawing lines ''' data_len = len(data) lines_list = [] if line_alternation is None: # For each line, generate a tuple giving to a line a color for each in range(data_len): lines_list.append(('LINEABOVE', (0, each), (-1, each), LINE_THICKNESS, LINE_COLOR)) # Last line lines_list.append(('LINEBELOW', (0, len(data) - 1), (-1, len(data) - 1), LINE_THICKNESS, LINE_COLOR)) elif line_alternation == []: # Do nothing return lines_list else: # if data_len > len(line_alternation) : # logger.warning("Line alternation for PDF display isn't correctly set. Looping on given values only.") # 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)]: lines_list.append(('LINEABOVE', (0, each), (-1, each), LINE_THICKNESS, LINE_COLOR)) # Last line lines_list.append(('LINEBELOW', (0, len(data) - 1), (-1, len(data) - 1), LINE_THICKNESS, LINE_COLOR)) return lines_list def general_style_generator(): ''' Create the general style (alignement, padding ...) of the table, copying the MISP'event's web_view. :return: a list of properties, usable in a TableStyle ''' lines_list = [] lines_list.append(('VALIGN', (0, 0), (-1, -1), 'MIDDLE')) lines_list.append(('LEFTPADDING', (0, 0), (-1, -1), 0)) lines_list.append(('RIGHTPADDING', (0, 0), (-1, -1), 0)) # VERTICAL_PADDING = 2 # lines_list.append(('TOPPADDING', (0, 0), (-1, -1), VERTICAL_PADDING)) # lines_list.append(('BOTTOMPADDING', (0, 0), (-1, -1), VERTICAL_PADDING)) return lines_list def internationalize_font(config=None): global FIRST_COL_FONT global SECOND_COL_FONT 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]] if fonts_path_custom and os.path.isfile(fonts_path_custom): registerFont(TTFont("custom_font", fonts_path_custom)) FIRST_COL_FONT = 'custom_font' SECOND_COL_FONT = 'custom_font' else: 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 : NotoSansCJKtc - DemiLight.ttf NotoSansCJKtc - Regular.ttf NotoSansCJKtc - Black.ttf NotoSansCJKtc - Light.ttf NotoSansCJKtc - Thin.ttf NotoSansCJKtc - Bold.ttf NotoSansCJKtc - Medium.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" if not font_path.is_dir() or not noto_bold.is_file() or not noto.is_file(): font_path.mkdir(parents=True, exist_ok=True) if not noto_bold.is_file(): bf = requests.get('https://github.com/MISP/pdf_fonts/raw/refs/heads/master/Noto_TTF/NotoSansCJKtc-Bold.ttf') with open(noto_bold, 'wb') as f: f.write(bf.content) if not noto.is_file(): rf = requests.get('https://github.com/MISP/pdf_fonts/raw/refs/heads/master/Noto_TTF/NotoSansCJKtc-DemiLight.ttf') with open(noto, 'wb') as f: f.write(rf.content) 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(f"Trying to load a custom (internationalization) font, unable to access the file: {noto_bold} / {noto}") def get_table_styles(): ''' Create and returns the two mains styles for the columns of the document. :return: two styles, one for each columns of the document, describing the MISP object. ''' sample_style_sheet = getSampleStyleSheet() custom_body_style_col_1 = ParagraphStyle(name='Column_1', parent=sample_style_sheet['Normal'], fontName=FIRST_COL_FONT, textColor=FIRST_COL_FONT_COLOR, fontSize=TEXT_FONT_SIZE, leading=LEADING_SPACE, alignment=FIRST_COL_ALIGNEMENT) custom_body_style_col_2 = ParagraphStyle(name='Column_2', parent=sample_style_sheet['Normal'], fontName=SECOND_COL_FONT, textColor=SECOND_COL_FONT_COLOR, fontSize=TEXT_FONT_SIZE, leading=LEADING_SPACE, alignment=SECOND_COL_ALIGNEMENT) return custom_body_style_col_1, custom_body_style_col_2 def get_clusters_table_styles(): ''' Create and returns the two mains styles for the columns of a table describing a cluster. :return: two styles, one for each columns of the document, describing the MISP object. ''' col1, col2 = get_table_styles() custom_body_style_col_1 = ParagraphStyle(name='Column_1_small', parent=col1, fontName=FIRST_COL_FONT, textColor=FIRST_COL_FONT_COLOR, fontSize=SMALL_FONT_SIZE, leading=SMALL_LEADING_SPACE, alignment=SMALL_COL1_ALIGMENT) custom_body_style_col_2 = ParagraphStyle(name='Column_2_small', parent=col2, fontName=SECOND_COL_FONT, textColor=SECOND_COL_FONT_COLOR, fontSize=SMALL_FONT_SIZE, leading=SMALL_LEADING_SPACE, alignment=SMALL_COL2_ALIGMENT) return custom_body_style_col_1, custom_body_style_col_2 ######################################################################## # Checks def safe_string(bad_str): return escape(str(bad_str)) def is_safe_value(value): return (value is not None and value != "") def is_safe_table(value): return (value is not None and value != []) 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 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] != "") 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) != []) def is_in_config(config, index): return config is not None and moduleconfig[index] in config ######################################################################## # Functions grouped by misp object type class Value_Formatter(): ''' "item" parameter should be as follow, 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"] ''' # ---------------------------------------------------------------------- def __init__(self, config, col1_style, col2_style, col1_small_style, col2_small_style): self.config = config self.col1_style = col1_style self.col2_style = col2_style self.col1_small_style = col1_small_style self.col2_small_style = col2_small_style # ---------------------------------------------------------------------- ######################################################################## # General attribut formater def get_col1_paragraph(self, dirty_string, do_small=False): if do_small: return self.get_unoverflowable_paragraph(dirty_string, self.col1_small_style, do_small=do_small) return self.get_unoverflowable_paragraph(dirty_string, self.col1_style, do_small=do_small) def get_unoverflowable_paragraph(self, dirty_string, curr_style=None, do_escape_string=True, do_small=False): ''' Create a paragraph that can fit on a cell displayed one page maximum. This method can be improved (get the exact size of the current frame, and limit the paragraph to this size.) KeepInFrame may give a nicer solution (not for me 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 = safe_string(dirty_string) else: sanitized_str = dirty_string if curr_style is None: if do_small: curr_style = self.col2_small_style else: curr_style = self.col2_style # 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 def get_value_link_to_event(self, uuid=None, text=None, curr_style=None, color=True): ''' Returns a flowable paragraph to add to the pdf given the misp_event uuid, with or without link :param curr_style: style to apply to the returned paragraph :param text: text to which the link will be anchored :param uuid: used to construct the link :param color: Boolean to give a color or not to the generate link (good link color) :return: a Paragraph to add in the pdf, regarding the values of "uuid" ''' if curr_style is None: curr_style = self.col2_style escape = True # Does MispEvent has the attribute ? if is_safe_value(text): # It has the requested attribute .. building upon it. # Does misp_object has an uuid and do we know the baseurl ? if is_safe_value(uuid) and is_in_config(self.config, 0): # We can build links escape = False curr_uuid = str(is_safe_value(uuid)) curr_baseurl = self.config[moduleconfig[0]] curr_url = uuid_to_url(curr_baseurl, curr_uuid) html_url = f"{safe_string(text)}" if color: # They want fancy colors html_url = f"{html_url}" # Construct final paragraph answer = html_url else: # We can't build links answer = text else: # No it doesn't, so we directly give the default answer answer = DEFAULT_VALUE 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 def get_date_value(self, date=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event date :param date: MISP_EVENT date to be formatted :return: a Paragraph to add in the pdf, regarding the values of "date" ''' answer = DEFAULT_VALUE if is_safe_value(date): answer = safe_string(date) return self.get_unoverflowable_paragraph(answer) def get_owner_value(self, owner=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event owner :param owner: MISP_EVENT owner to be formatted :return: a Paragraph to add in the pdf, regarding the values of "owner" ''' answer = DEFAULT_VALUE if is_safe_value(owner): answer = safe_string(owner) return self.get_unoverflowable_paragraph(answer) def get_threat_value(self, threat_level=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event threat :param threat_level: MISP_EVENT threat level (int) to be formatted :return: a Paragraph to add in the pdf, regarding the values of "threat" ''' answer = "No threat level specified." if is_safe_value(threat_level) and str(threat_level) in threat_map: answer = threat_map[safe_string(threat_level)] return self.get_unoverflowable_paragraph(answer, do_escape_string=False) def get_analysis_value(self, analysis_level=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event analysis :param analysis_level: MISP_EVENT analysis level (int) to be formatted :return: a Paragraph to add in the pdf, regarding the values of "analysis" ''' answer = "No analysis status specified." if is_safe_value(analysis_level) and str(analysis_level) in analysis_map: answer = analysis_map[safe_string(analysis_level)] return self.get_unoverflowable_paragraph(answer, do_escape_string=False) def get_timestamp_value(self, timestamp=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event timestamp :param timestamp: MISP_EVENT timestamp (int) to be formatted :return: a Paragraph to add in the pdf, regarding the values of "timestamp" ''' answer = "No timestamp specified." if is_safe_value(timestamp): answer = safe_string(timestamp.strftime(EXPORT_DATE_FORMAT)) return self.get_unoverflowable_paragraph(answer) def get_creator_organisation_value(self, creator=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event creator organisation :param creator: MISP_EVENT creator (not the name directly) to be formatted :return: a Paragraph to add in the pdf, regarding the values of "creator organisation" ''' answer = DEFAULT_VALUE if is_safe_value(creator) and is_safe_value(creator.get('name', None)): answer = safe_string(creator.get('name', None)) return self.get_unoverflowable_paragraph(answer) def get_attributes_number_value(self, attributes=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event attributes :param attributes: MISP_EVENT attributes list to be formatted :return: a Paragraph to add in the pdf, regarding the values of "attributes" ''' answer = "0 - no attribute" if is_safe_table(attributes): answer = safe_string(len(attributes)) return self.get_unoverflowable_paragraph(answer) def get_published_value(self, published_bool=None, published_timestamp=None): ''' 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 published_timestamp: MISP_EVENT published_timestamp to be formatted :param published_bool: MISP_EVENT published boolean value :return: a Paragraph to add in the pdf, regarding the values of "published"/"publish_timestamp" ''' # Formatting similar to MISP Event web view if is_safe_value(published_bool): if published_bool: # == True answer = YES_ANSWER if is_safe_value(published_timestamp): # Published and have published date answer += f'({published_timestamp.strftime(EXPORT_DATE_FORMAT)})' else: # Published without published date answer += "(no date)" else: # Not published answer = NO_ANSWER else: # Does not have a published attribute answer = DEFAULT_VALUE return self.get_unoverflowable_paragraph(answer, do_escape_string=False) def get_image_value(self, image_buffer=None): ''' Returns a flowable image to add to the pdf given the misp attribute type and data :param image_buffer: an image contained an attribute, for example (buffer / Base 64) :return: a flowable image to add in the pdf, regarding the values of "data" ''' try: # Get the image buf = image_buffer # TODO : Do verification on the buffer ? # Create image within a bounded box (to allow pdf creation) img = Image(buf, width=FRAME_PICTURE_MAX_WIDTH, height=FRAME_PICTURE_MAX_HEIGHT, kind='bound') 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(f"{NOT_A_PICTURE_MESSAGE}", do_escape_string=False) return answer def get_good_link(self, value=None): ''' Returns a flowable paragraph to add to the pdf given the misp_attribute value, if this is a link :param value: string, of an url to format as a good link :return: a Paragraph to add in the pdf, regarding the values of this "link" attribute ''' return self.get_unoverflowable_paragraph(f"{value}", do_escape_string=False) def get_bad_link(self, value=None): ''' Returns a flowable paragraph to add to the pdf given the misp_attribute value, if this is a link :param value: string, of an url to format as a bad link :return: a Paragraph to add in the pdf, regarding the values of this "url" attribute ''' return self.get_unoverflowable_paragraph(f"{value}", do_escape_string=False) def get_good_or_bad_link(self, value=None, type=None): ''' Returns a flowable paragraph to add to the pdf given the misp_attribute value, if this is a link or an url :param type: Type of the url (url or link) as a string :param value: string, of an url to format as a good or bad link :return: a Paragraph to add in the pdf, regarding the values of this "link" or "url" attribute ''' answer = self.get_unoverflowable_paragraph("Not an URL") # Handle "Good" links if type == LINK_TYPE: answer = self.get_good_link(value=value) # Handle "bad "links elif type == URL_TYPE: answer = self.get_bad_link(value=value) return answer def get_galaxy_name_value(self, misp_galaxy): ''' Create a displayable name for a galaxy :param misp_galaxy: MISP_EVENT galaxy, as an object (not a list) :return: a Flowable Paragraph to add in the pdf, regarding the value of the MISP galaxy ''' answer = DEFAULT_VALUE if is_safe_dict_attribute(misp_galaxy, 'name'): answer = '{} from {}:{}'.format(safe_string(misp_galaxy['name']), safe_string(misp_galaxy["namespace"]), safe_string(misp_galaxy["type"])) return self.get_unoverflowable_paragraph(answer, do_small=True) def get_galaxy_cluster_name_value(self, misp_cluster, do_small=False): ''' Create a displayable name for a cluster :param misp_cluster: a MISP_EVENT's GALAXY's cluster, as an object (not a list) :param do_small: Compress the display (reduce the size of the flowable paragraph, fonts, etc.) :return: a Flowable Paragraph to add in the pdf, regarding the value of the MISP cluster ''' # TODO : To be changed when Clust becomes an object tmp_text = "" if is_safe_dict_attribute(misp_cluster, 'value'): tmp_text += safe_string(misp_cluster['value']) # if is_safe_dict_attribute(misp_cluster, item[3]) : # tmp_text += "
Source : " + misp_cluster[item[3]] if is_safe_dict_attribute(misp_cluster, "meta") and is_safe_dict_attribute(misp_cluster["meta"], "synonyms"): tmp_text += "
Synonyms : " for i, synonyme in enumerate(misp_cluster["meta"]["synonyms"]): if i != 0: tmp_text += " / " tmp_text += safe_string(synonyme) return self.get_unoverflowable_paragraph(tmp_text, do_escape_string=False, do_small=do_small) return self.get_unoverflowable_paragraph(DEFAULT_VALUE, do_small=do_small) class Event_Metadata(): # ---------------------------------------------------------------------- def __init__(self, config, value_formatter): self.config = config self.value_formatter = value_formatter self.sample_style_sheet = getSampleStyleSheet() # ---------------------------------------------------------------------- ######################################################################## # General Event's Attributes formater def create_flowable_table_from_event(self, misp_event): ''' 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 ''' data = [] flowable_table = [] # Manual addition # UUID data.append([self.value_formatter.get_col1_paragraph("UUID"), self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid', None), text=misp_event.get('uuid', None))]) # Date data.append({self.value_formatter.get_col1_paragraph("Date"), self.value_formatter.get_date_value(date=misp_event.get('date', None))}) # Owner data.append([self.value_formatter.get_col1_paragraph("Owner org"), self.value_formatter.get_owner_value(owner=misp_event.get('owner', None))]) # Threat data.append([self.value_formatter.get_col1_paragraph("Threat level"), self.value_formatter.get_threat_value(threat_level=misp_event.get('threat_level_id', None))]) # Analysis data.append([self.value_formatter.get_col1_paragraph("Analysis"), self.value_formatter.get_analysis_value(analysis_level=misp_event.get('analysis', None))]) # Info data.append([self.value_formatter.get_col1_paragraph("Info"), self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid', None), text=misp_event.get('info', None))]) # Timestamp data.append([self.value_formatter.get_col1_paragraph("Event date"), self.value_formatter.get_timestamp_value(timestamp=misp_event.get('timestamp', None))]) # Published data.append([self.value_formatter.get_col1_paragraph("Published"), self.value_formatter.get_published_value(published_bool=misp_event.get('published', None), published_timestamp=misp_event.get('publish_timestamp', None))]) # Creator organisation data.append([self.value_formatter.get_col1_paragraph("Creator Org"), self.value_formatter.get_creator_organisation_value(creator=misp_event.get('Orgc', None))]) # Number of Attributes data.append([self.value_formatter.get_col1_paragraph("# Attributes"), self.value_formatter.get_attributes_number_value(attributes=misp_event.get('Attribute', None))]) # Tags curr_Tags = Tags(self.config, self.value_formatter) data.append([self.value_formatter.get_col1_paragraph("Tags"), curr_Tags.get_tag_value(tags=misp_event.get('Tag', None))]) flowable_table.append(create_flowable_table_from_data(data)) # Correlation if is_safe_table(misp_event.get('RelatedEvent', None)) and is_in_config(self.config, 4): flowable_table += self.get_correlation_values(related_events=misp_event.get('RelatedEvent', None)) # Galaxies if is_safe_attribute_table(misp_event, "Related Galaxies") and is_in_config(self.config, 3): flowable_table.append(PageBreak()) curr_Galaxy = Galaxy(self.config, self.value_formatter) flowable_table += curr_Galaxy.get_galaxy_value(galaxies=misp_event.get('Galaxy', None)) return flowable_table def create_reduced_flowable_table_from_event(self, misp_event): ''' 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 ''' data = [] flowable_table = [] # Manual addition # UUID data.append([self.value_formatter.get_col1_paragraph("UUID"), self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid', None), text=misp_event.get('uuid', None))]) # Info data.append([self.value_formatter.get_col1_paragraph("Info"), self.value_formatter.get_value_link_to_event(uuid=misp_event.get('uuid', None), text=misp_event.get('info', None))]) # Timestamp data.append([self.value_formatter.get_col1_paragraph("Event date"), self.value_formatter.get_timestamp_value(timestamp=misp_event.get('timestamp', None))]) flowable_table.append(create_flowable_table_from_data(data)) return flowable_table def create_flowable_description_from_event(self, misp_event): ''' Returns a Paragraph presenting a MISP event :param misp_event: A misp event (complete or not) :return: a paragraph that can be added to a pdf ''' ''' The event "{EventName}" | that occurred on {EventDate}, | had been shared by {Organisation Name} | on the {Date}. ''' text = "" if is_safe_attribute(misp_event, 'info'): text += "The event '" text += safe_string(misp_event.info) text += "'" else: text += "This event" if is_safe_attribute(misp_event, 'timestamp'): text += " that occurred on " text += safe_string(misp_event.timestamp.strftime(EXPORT_DATE_FORMAT)) text += "," text += " had been shared by " if is_safe_attribute(misp_event, 'Orgc') and is_safe_attribute(misp_event, 'name'): text += safe_string(misp_event.Orgc.name) else: text += " an unknown organisation" if is_safe_attribute(misp_event, 'date'): text += " on the " text += safe_string(misp_event.date) else: text += " on an unknown date" text += "." ''' The threat level of this event is {ThreatLevel} and the analysis that was made of this event is {AnalysisLevel}. ''' text += " The threat level of this event is " if is_safe_attribute(misp_event, 'threat_level_id') and safe_string(misp_event.threat_level_id) in threat_map: text += threat_map[safe_string(misp_event.threat_level_id)] else: text += " unknown" text += " and the analysis that was made of this event is " if is_safe_attribute(misp_event, 'analysis') and safe_string(misp_event.analysis) in analysis_map: text += analysis_map[safe_string(misp_event.analysis)] else: text += " undefined" text += "." ''' The event is currently {Published} and has associated attributes {Attribute Number}. ''' text += " The event is currently " if is_safe_attribute(misp_event, 'published') and misp_event.published: text += " published" if is_safe_attribute(misp_event, 'publish_timestamp'): text += " since " + misp_event.publish_timestamp.strftime(EXPORT_DATE_FORMAT) else: text += " private" # Number of Attributes text += ", has " if is_safe_attribute_table(misp_event, 'Attribute'): text += safe_string(len(misp_event.Attribute)) else: text += " 0" text += " associated attributes" # Number of Objects text += " and has " if is_safe_attribute_table(misp_event, 'Object'): text += safe_string(len(misp_event.Object)) else: text += " 0" text += " associated objects." curr_attributes = Attributes(self.config, self.value_formatter) tmp_text = curr_attributes.get_external_analysis(misp_event) if tmp_text != "": text += "
" text += tmp_text text += "
" ''' For more information on the event, please consult the rest of the document ''' text += "
For more information on the event, please consult following information." description_style = ParagraphStyle(name='Description', parent=self.value_formatter.col2_style, alignment=TA_JUSTIFY) return Paragraph(text, description_style) def get_correlation_values(self, related_events=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event correlated events :return: a Paragraph to add in the pdf, regarding the values of "RelatedEvent" ''' flowable_table = [] flowable_table.append(PageBreak()) flowable_table.append(Paragraph("Related Events", self.sample_style_sheet['Heading3'])) if is_safe_table(related_events): for i, evt in enumerate(related_events): flowable_table.append(Indenter(left=INDENT_SIZE_HEADING)) flowable_table.append( Paragraph("Related Event #" + str(i + OFFSET), self.sample_style_sheet['Heading4'])) flowable_table.append(Indenter(left=-INDENT_SIZE_HEADING)) flowable_table += self.create_reduced_flowable_table_from_event(evt['Event']) i += 1 else: return flowable_table.append(self.value_formatter.get_unoverflowable_paragraph(DEFAULT_VALUE)) return flowable_table class Attributes(): # ---------------------------------------------------------------------- def __init__(self, config, value_formatter): self.config = config self.value_formatter = value_formatter self.sample_style_sheet = getSampleStyleSheet() # ---------------------------------------------------------------------- def create_flowable_table_from_attributes(self, 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 = [] i = 0 if is_safe_attribute_table(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(Indenter(left=INDENT_SIZE_HEADING)) flowable_table.append(Paragraph("Attribute #" + str(i + OFFSET), self.sample_style_sheet['Heading4'])) flowable_table.append(Indenter(left=-INDENT_SIZE_HEADING)) flowable_table += self.create_flowable_table_from_one_attribute(item) i += 1 else: # No attributes for this object flowable_table.append(Paragraph("No attributes", self.sample_style_sheet['Heading4'])) return flowable_table def create_flowable_table_from_one_attribute(self, 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 = [] flowable_table = [] # 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 is_safe_value(misp_attribute.get('type', None)) and (misp_attribute.type in [LINK_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([self.value_formatter.get_col1_paragraph(item[0]), self.value_formatter.get_unoverflowable_paragraph(getattr(misp_attribute, item[1]))]) # 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) - There were not written in the previous loop if not STANDARD_TYPE and is_safe_value(misp_attribute.get('value', None)): data.append([self.value_formatter.get_col1_paragraph("Value"), self.value_formatter.get_good_or_bad_link(value=misp_attribute.get('value', None), type=misp_attribute.get('type', None))]) # Handle pictures if is_safe_value(misp_attribute.get('data', None)) and misp_attribute.type == IMAGE_TYPE: data.append([self.value_formatter.get_col1_paragraph("Data"), self.value_formatter.get_image_value(misp_attribute.get('data', None))]) # Tags curr_Tags = Tags(self.config, self.value_formatter) if is_safe_table(misp_attribute.get('Tag', None)): data.append( [self.value_formatter.get_col1_paragraph("Tags"), curr_Tags.get_tag_value(tags=misp_attribute.get('Tag', None))]) # Sighting curr_Sighting = Sightings(self.config, self.value_formatter) if is_safe_table(misp_attribute.get('Sighting', None)): data.append([self.value_formatter.get_col1_paragraph("Sighting"), curr_Sighting.create_flowable_paragraph_from_sightings(sightings=misp_attribute.get('Sighting', None))]) flowable_table.append(create_flowable_table_from_data(data)) # Galaxies if is_safe_attribute_table(misp_attribute, "Galaxy") and is_in_config(self.config, 3): curr_Galaxy = Galaxy(self.config, self.value_formatter) flowable_table.append(Indenter(left=INDENT_SIZE)) flowable_table += curr_Galaxy.get_galaxy_value(misp_attribute.get('Galaxy', None)) flowable_table.append(Indenter(left=-INDENT_SIZE)) return flowable_table def get_external_analysis(self, misp_event): ''' Returns a string representing the list of external analysis comments of a misp event. :param misp_event: A misp event :return: a table of flowables ''' text = "" if is_safe_attribute_table(misp_event, "Attribute"): # 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"): # We add it to the description text += "
" + EXTERNAL_ANALYSIS_PREFIX + safe_string(getattr(attribute, "value")) return text class Tags(): # ---------------------------------------------------------------------- def __init__(self, config, value_formatter): self.config = config self.value_formatter = value_formatter # ---------------------------------------------------------------------- def get_tag_value(self, tags=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event tags :return: a Paragraph to add in the pdf, regarding the values of "tags" ''' if is_safe_table(tags): table_event_tags = self.create_flowable_table_from_tags(tags=tags) return table_event_tags return self.value_formatter.get_unoverflowable_paragraph(DEFAULT_VALUE) def create_flowable_table_from_tags(self, tags=None): ''' 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 = [] i = 0 if is_safe_table(tags): # There is some tags for this object for curr_tag in tags: flowable_table.append(create_flowable_tag(curr_tag)) i += 1 answer_tags = self.create_tags_table_from_data(flowable_table) else: # No tags for this object answer_tags = [self.value_formatter.get_unoverflowable_paragraph("No tags")] return answer_tags def create_tags_table_from_data(self, 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 class Sightings(): # ---------------------------------------------------------------------- def __init__(self, config, value_formatter): self.config = config self.value_formatter = value_formatter # ---------------------------------------------------------------------- def create_flowable_paragraph_from_sightings(self, sightings=None): ''' Returns a Table (flowable) to add to a pdf, representing the list of sightings of an event or a misp event :param misp_event: A misp event :return: a table of flowable to add to the pdf ''' i = 0 # No tags for this object answer = "No sighting" list_sighting = [0, 0, 0] if is_safe_table(sightings): # There is some tags for this object for curr_item in sightings: # TODO : When Sightings will be object : if is_safe_attribute(item, "type"): if is_safe_dict_attribute(curr_item, "type"): # Store the likes/dislikes depending on their types list_sighting[int(curr_item["type"])] += 1 i += 1 # Create the sighting text sight_text = f" Positive: {list_sighting[0]}" sight_text += f" / Negative: {list_sighting[1]}" sight_text += f" / Misc.: {list_sighting[2]}" answer = sight_text return self.value_formatter.get_unoverflowable_paragraph(answer, do_escape_string=False) class Object(): # ---------------------------------------------------------------------- def __init__(self, config, value_formatter): self.config = config self.value_formatter = value_formatter self.sample_style_sheet = getSampleStyleSheet() # ---------------------------------------------------------------------- def create_flowable_table_from_objects(self, objects=None): ''' Returns a list of flowables representing the list of objects of a misp event. The list is composed of a serie of [ header object, table of object information, [ header of attribute, table of attribute]*] to add to the pdf :param misp_event: A misp event :return: a table of flowables ''' flowable_table = [] i = 0 if is_safe_table(objects): # There is a list of objects for item in objects: # you can use a spacer instead of title to separate paragraph: flowable_table.append(Spacer(1, 5 * mm)) flowable_table.append(Indenter(left=INDENT_SIZE_HEADING)) flowable_table.append(Paragraph("Object #" + str(i + OFFSET), self.sample_style_sheet['Heading3'])) flowable_table.append(Indenter(left=-INDENT_SIZE_HEADING)) flowable_table += self.create_flowable_table_from_one_object(item, self.config) i += 1 else: # No object found flowable_table.append(Paragraph("No object", self.sample_style_sheet['Heading3'])) return flowable_table def create_flowable_table_from_one_object(self, misp_object, config=None): ''' Returns a table (flowable) representing the object :param misp_attribute: A misp object :return: a table representing this misp's object's attributes, to add to the pdf as a flowable ''' data = [] # 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"], ["Description", 'description', "None"], ["Meta Category", 'meta-category', "None"], ["Object Name", 'name', "None"], ["Comment", 'comment', "None"], ["Type", 'type', "None"]] # Automated adding of standard (python) attributes of the misp object for item in list_attr_automated: if is_safe_attribute(misp_object, item[1]): # The attribute exists, we fetch it and create the row data.append([self.value_formatter.get_col1_paragraph(item[0]), self.value_formatter.get_unoverflowable_paragraph(getattr(misp_object, item[1]))]) # 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)]) # Timestamp data.append([self.value_formatter.get_col1_paragraph("Object date"), self.value_formatter.get_timestamp_value(misp_object.get('timestamp', None))]) # Transform list of value in a table data = [create_flowable_table_from_data(data)] # Handle all the attributes if is_safe_value(misp_object.get("Attribute", None)): curr_attributes = Attributes(self.config, self.value_formatter) data.append(Indenter(left=INDENT_SIZE)) data += curr_attributes.create_flowable_table_from_attributes(misp_object) data.append(Indenter(left=-INDENT_SIZE)) # Add a page break at the end of an object data.append(PageBreak()) return data class Galaxy(): # ---------------------------------------------------------------------- def __init__(self, config, value_formatter): self.config = config self.value_formatter = value_formatter self.sample_style_sheet = getSampleStyleSheet() # ---------------------------------------------------------------------- def get_galaxy_value(self, galaxies=None): ''' Returns a flowable paragraph to add to the pdf given the misp_event galaxies :return: a Flowable to add in the pdf, regarding the values of "galaxies" ''' flowable_table = [] # Galaxies # item = ["Related Galaxies", 'Galaxy', "None"] if is_safe_table(galaxies) and is_in_config(self.config, 3): galaxy_title = Paragraph(safe_string("Related Galaxies"), self.sample_style_sheet['Heading5']) flowable_table.append(Indenter(left=INDENT_SIZE_HEADING)) flowable_table.append(galaxy_title) flowable_table.append(Indenter(left=-INDENT_SIZE_HEADING)) flowable_table += self.create_flowable_table_from_galaxies(galaxies=galaxies) else: flowable_table.append(self.value_formatter.get_unoverflowable_paragraph(DEFAULT_VALUE)) return flowable_table def create_flowable_table_from_galaxies(self, galaxies=None): ''' Returns a Table (flowable) to add to a pdf, representing the list of galaxies of an event or a misp event :param misp_event: A misp event :return: a table of flowables to add to the pdf ''' flowable_table = [] scheme_alternation = [] curr_color = 0 i = 0 small_title_style = ParagraphStyle(name='Column_1', parent=self.sample_style_sheet['Heading6'], fontName=FIRST_COL_FONT, alignment=TA_LEFT) if is_safe_table(galaxies): # There is some galaxies for this object for curr_galaxy in galaxies: # For each galaxy of the misp object txt_title = "Galaxy #" + str(i + OFFSET) + " - " + safe_string(curr_galaxy["name"]) galaxy_title = Paragraph(txt_title, small_title_style) flowable_table.append(Indenter(left=INDENT_SIZE_HEADING)) flowable_table.append(galaxy_title) flowable_table.append(Indenter(left=-INDENT_SIZE_HEADING)) i += 1 # Add metadata about the Galaxy galaxy_metadata, nb_added_item = self.create_flowable_table_from_one_galaxy(curr_galaxy) flowable_table += galaxy_metadata # Construct the line color scheme and line scheme scheme_alternation += [curr_color] * nb_added_item # Add metadata about clusters curr_cluster = Galaxy_cluster(self.config, self.value_formatter) 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")] flowable_table.append(create_flowable_table_from_data(answer_tags)) return flowable_table def create_flowable_table_from_one_galaxy(self, misp_galaxy): ''' Returns a table (flowable) representing the galaxy :param misp_galaxy: A misp galaxy :return: a table representing this misp's galaxy's attributes, to add to the pdf as a flowable ''' data = [] nb_added_item = 0 # Name if is_safe_value(misp_galaxy.get('name', None)): data.append([self.value_formatter.get_col1_paragraph("Name", do_small=DO_SMALL_GALAXIES), self.value_formatter.get_galaxy_name_value(misp_galaxy)]) nb_added_item += 1 # Description if is_safe_value(misp_galaxy.get('description', None)): data.append([self.value_formatter.get_col1_paragraph("Description", do_small=DO_SMALL_GALAXIES), self.value_formatter.get_unoverflowable_paragraph(misp_galaxy.get('description', None), do_small=DO_SMALL_GALAXIES)]) nb_added_item += 1 flowable_table = [] flowable_table.append(create_flowable_table_from_data(data)) return flowable_table, nb_added_item class Galaxy_cluster(): # ---------------------------------------------------------------------- def __init__(self, config, value_formatter): self.config = config self.value_formatter = value_formatter self.sample_style_sheet = getSampleStyleSheet() # ---------------------------------------------------------------------- def create_flowable_table_from_galaxy_clusters(self, misp_galaxy): ''' Returns a Table (flowable) to add to a pdf, representing the list of galaxy clusters of a galaxy :param misp_event: A misp event :return: a table of flowables to add to the pdf ''' data = [] tmp_title = "Cluster #" if is_safe_value(misp_galaxy.get("GalaxyCluster", None)): # There is some clusters for this object for i, curr_cluster in enumerate(misp_galaxy.get("GalaxyCluster", None)): # If title is needed : # galaxy_title = [Paragraph("Cluster #" + str(i), self.sample_style_sheet['Heading6'])] # data.append(galaxy_title) tmp_title = "Cluster #" + str(i + OFFSET) # For each cluster tmp_data = self.create_flowable_table_from_one_galaxy_cluster(curr_cluster) tmp_flowable_table = [] tmp_flowable_table.append(create_flowable_table_from_data(tmp_data, col_w=SECOND_LEVEL_GALAXY_WIDTHS, color_alternation=CLUSTER_COLORS, line_alternation=[], galaxy_colors=True)) data.append([self.value_formatter.get_col1_paragraph(tmp_title, do_small=DO_SMALL_GALAXIES), tmp_flowable_table]) # Cluster #X - 3 lines else: # No galaxies for this object data = [self.value_formatter.get_unoverflowable_paragraph("No galaxy cluster", do_small=DO_SMALL_GALAXIES)] flowable_table = [] flowable_table.append( create_flowable_table_from_data(data, col_w=FIRST_LEVEL_GALAXY_WIDTHS, color_alternation=CLUSTER_COLORS, galaxy_colors=True)) return flowable_table def create_flowable_table_from_one_galaxy_cluster(self, misp_cluster): ''' Returns a table (flowable) representing a galaxy cluster :param misp_attribute: A misp galaxy :return: a table representing this misp's galaxy's cluster attributes, to add to the pdf as a flowable ''' data = [] # Name data.append([self.value_formatter.get_col1_paragraph("Name", do_small=True), self.value_formatter.get_galaxy_cluster_name_value(misp_cluster, do_small=True)]) if misp_cluster['value'] != misp_cluster['description']: # Prevent name that are same as description # Description data.append([self.value_formatter.get_col1_paragraph("Description", do_small=True), self.value_formatter.get_unoverflowable_paragraph(misp_cluster.get('description', None), do_small=True)]) # Refs ? # item = ["Description", 'description', "None"] # data.append([self.value_formatter.get_col1_paragraph(item[0]), # self.value_formatter.get_unoverflowable_paragraph(misp_cluster[item[1]])]) return data ######################################################################## # Handling static parts drawn on the upper layer class Statics_Drawings(): # ---------------------------------------------------------------------- def __init__(self, config, misp_event): self.config = config self.misp_event = misp_event # ---------------------------------------------------------------------- def set_template(self, canvas, doc): self.add_page_number(canvas, doc) self.add_metadata(canvas, doc) # TODO : add_header() # TODO : add_footer() def add_metadata(self, canvas, doc): ''' 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 ''' if is_safe_attribute(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(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(self.misp_event.Orgc.name) if is_safe_attribute(self.misp_event, 'uuid'): canvas.setKeywords(self.misp_event.uuid) def add_page_number(self, canvas, doc): ''' 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 ''' canvas.saveState() canvas.setFont('Times-Roman', 10) page_number_text = "%d" % (doc.page) curr_spacing = 4 * mm # 0.75 * inch canvas.drawCentredString( curr_spacing, curr_spacing, page_number_text ) canvas.restoreState() ######################################################################## # 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() col1_style, col2_style = get_table_styles() col1_small_style, col2_small_style = get_clusters_table_styles() 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 = curr_val_f.get_value_link_to_event(uuid=misp_event.get('uuid', None), text=misp_event.get('info', None), curr_style=title_style, color=False) # Add all parts to final PDF flowables.append(title) # Creation of handling objects curr_event = Event_Metadata(config, curr_val_f) curr_attr = Attributes(config, curr_val_f) curr_object = Object(config, curr_val_f) if is_in_config(config, 2): # If description is activated description = Paragraph("Description", sample_style_sheet['Heading2']) description_text = curr_event.create_flowable_description_from_event(misp_event) flowables.append(description) flowables.append(description_text) subtitle = Paragraph("General information", sample_style_sheet['Heading2']) table_general_metainformation = curr_event.create_flowable_table_from_event(misp_event) flowables.append(subtitle) flowables += table_general_metainformation if is_safe_attribute_table(misp_event, "Attribute"): flowables.append(PageBreak()) event_attributes_title = Paragraph("Attributes", sample_style_sheet['Heading2']) table_direct_attributes = curr_attr.create_flowable_table_from_attributes(misp_event) flowables.append(event_attributes_title) flowables += table_direct_attributes if is_safe_attribute_table(misp_event, "Object"): flowables.append(PageBreak()) event_objects_title = Paragraph("Objects", sample_style_sheet['Heading2']) table_objects = curr_object.create_flowable_table_from_objects(objects=misp_event.get("Object", None)) flowables.append(event_objects_title) flowables += table_objects # If you want to output the full json (as debug), just add next line and add it to flowables # paragraph_2 = Paragraph(str(misp_event.to_json()), sample_style_sheet['Code']) return flowables def export_flowables_to_pdf(document, misp_event, flowables, config): ''' Export function : creates a pdf from a list of flowables, adding page numbers, etc. :param document: A document template :param pdf_buffer: / not used :param flowables: list of flowables to compile as pdf :return: ''' static_drawer = Statics_Drawings(config, misp_event) document.build( flowables, # Partial used to set the metadata onFirstPage=static_drawer.set_template, # Pagination for first page onLaterPages=static_drawer.set_template, # Pagination for all other page ) # Old way : onLaterPages=partial(static_drawer.set_template, misp_event=misp_event), # Pagination for all other page ######################################################################## # "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 :return: a pdf buffer (BytesIO) that contains the pdf ''' # Create a document buffer pdf_buffer = BytesIO() if is_in_config(config, 5): # We want internationalization logger.info("Internationalization of fonts during pdf export activated. CJK-fonts supported.") internationalize_font(config) # DEBUG / TO DELETE : curr_document = SimpleDocTemplate('myfile.pdf') curr_document = SimpleDocTemplate(pdf_buffer, pagesize=PAGESIZE, topMargin=BASE_MARGIN, leftMargin=BASE_MARGIN, rightMargin=BASE_MARGIN, bottomMargin=BASE_MARGIN) # Collect already accessible event's parts to be shown flowables = collect_parts(misp_event, config) # Export export_flowables_to_pdf(curr_document, misp_event, flowables, config) pdf_value = pdf_buffer.getvalue() # Not sure what to give back ? Buffer ? Buffer.value() ? Base64(buffer.value()) ? ... So far only buffer.value() pdf_buffer.close() return pdf_value 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) 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)