mirror of https://github.com/MISP/PyMISP
general improvement : deisgn, exhaustiviness of mispEvent values displayed, good pratice concerning paragraphe/table made
parent
01b2ad9199
commit
0fc780994f
|
@ -5,6 +5,7 @@
|
|||
from io import BytesIO
|
||||
import base64
|
||||
import logging
|
||||
import pymisp
|
||||
|
||||
logger = logging.getLogger('pymisp')
|
||||
|
||||
|
@ -22,98 +23,317 @@ try:
|
|||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import letter, inch
|
||||
from reportlab.lib.enums import TA_RIGHT, TA_CENTER, TA_JUSTIFY, TA_LEFT
|
||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
|
||||
HAS_REPORTLAB = True
|
||||
except ImportError:
|
||||
HAS_REPORTLAB = False
|
||||
print("ReportLab cannot be imported. Please verify that ReportLab is installed on the system.")
|
||||
|
||||
'''
|
||||
"UTILITIES" METHODS. Not meant to be used except for development purposes
|
||||
'''
|
||||
import pprint
|
||||
|
||||
|
||||
def get_sample_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, 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()
|
||||
|
||||
|
||||
'''
|
||||
"INTERNAL" METHODS. Not meant to be used outside of this class.
|
||||
'''
|
||||
|
||||
def create_flowable_table_from_event(misp_event):
|
||||
|
||||
data = [['00', '01', '02', '03', '04'],
|
||||
['10', '11', '12', '13', '14'],
|
||||
['20', '21', '22', '23', '24'],
|
||||
['30', '31', '32', '33', '34']]
|
||||
|
||||
t = Table(data, 5 * [0.4 * inch], 4 * [0.4 * inch])
|
||||
|
||||
t.setStyle(TableStyle([('TEXTCOLOR', (0, 0), (0, -1), colors.blue),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
('INNERGRID', (0, 0), (-1, -1), 0.25, colors.black),
|
||||
('BOX', (0, 0), (-1, -1), 0.25, colors.black),
|
||||
]))
|
||||
|
||||
return t
|
||||
EVEN_COLOR = colors.whitesmoke
|
||||
ODD_COLOR = colors.lightgrey
|
||||
|
||||
|
||||
def collect_parts(misp_event):
|
||||
def alternate_colors_style_generator(data):
|
||||
# Modified from : https://gist.github.com/chadcooper/5798392
|
||||
|
||||
data_len = len(data)
|
||||
color_list = []
|
||||
|
||||
# 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
|
||||
else:
|
||||
bg_color = ODD_COLOR
|
||||
color_list.append(('BACKGROUND', (0, each), (-1, each), bg_color))
|
||||
|
||||
return color_list
|
||||
|
||||
|
||||
LINE_COLOR = colors.lightslategray
|
||||
LINE_THICKNESS = 0.75
|
||||
|
||||
def lines_style_generator(data):
|
||||
data_len = len(data)
|
||||
lines_list = []
|
||||
|
||||
# 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))
|
||||
|
||||
return lines_list
|
||||
|
||||
# FIRST_COL_FONT_COLOR = colors.darkslateblue # Test purposes
|
||||
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
|
||||
EXPORT_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
COL_WIDTHS = ['30%', '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)
|
||||
|
||||
def get_published_value(misp_event):
|
||||
'''
|
||||
:param misp_event: A misp event with or without "published"/"publish_timestamp" attributes
|
||||
:return: a string to print in the pdf, regarding the values of "published"/"publish_timestamp"
|
||||
# More information on how to play with paragraph into reportlab cells : https://stackoverflow.com/questions/11810008/reportlab-add-two-paragraphs-into-one-table-cell
|
||||
'''
|
||||
|
||||
item = ["Published", 'published', "None", "publish_timestamp"]
|
||||
_, col2_style = get_table_styles()
|
||||
RED_COLOR = '#ff0000'
|
||||
GREEN_COLOR = '#008000'
|
||||
YES_ANSWER = "<font color=" + GREEN_COLOR + "><b> Yes </b></font> ("
|
||||
NO_ANSWER = "<font color=" + RED_COLOR + "><b>No</b></font>"
|
||||
|
||||
# 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 create_flowable_table_from_event(misp_event: pymisp.MISPEvent):
|
||||
# == Run on >1000 OSINT Events ==
|
||||
# 'Tag': 1065, OK
|
||||
# 'Attribute': 1050, NOT OK
|
||||
# 'Object': 175, NOT OK
|
||||
# 'info': 1065, OK
|
||||
# 'threat_level_id': 1065, OK (added) TODO : improve design
|
||||
# 'analysis': 1065, OK (added) TODO : improve design + Ask where the enum is !
|
||||
# 'published': 1065, OK (added)
|
||||
# 'date': 1065, OK (added)
|
||||
# 'timestamp': 1065, OK (added)
|
||||
# 'publish_timestamp': 1065, OK (added)
|
||||
# 'Orgc': 1065, OK
|
||||
# 'uuid': 1065 OK (added)
|
||||
|
||||
# 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"],
|
||||
["UUID", 'uuid', "None"], # OK
|
||||
["Creator org", 'org', "None"],
|
||||
["Date", 'date', "None"],
|
||||
["Owner org", 'owner', "None"],
|
||||
["Email", 'email', "None"],
|
||||
["Tags", 'TODO', "None"],
|
||||
["Threat level", 'threat_level_id', "None"],
|
||||
["Analysis", 'analysis', "None"],
|
||||
["Distribution", 'distribution', "None"],
|
||||
["Info", 'info', "None"], # OK
|
||||
["# Attributes", 'attribute_count', "None"],
|
||||
["First recorded change", 'TODO', "None"],
|
||||
["Last change", 'TODO', "None"],
|
||||
["Modification map", 'TODO', "None"],
|
||||
["Sightings", 'TODO', "None"]
|
||||
]
|
||||
|
||||
list_attr_manual = [["Event date", 'timestamp', "None"], # OK
|
||||
["Published", 'published', "None"], # OK
|
||||
["Sightings", 'TODO', "None"]
|
||||
]
|
||||
|
||||
data = []
|
||||
col1_style, col2_style = get_table_styles()
|
||||
|
||||
# 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), Paragraph(str(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 of specific attributes
|
||||
item = list_attr_manual[0] # Timestamp
|
||||
if hasattr(misp_event, item[1]):
|
||||
data.append([Paragraph(item[0], col1_style), Paragraph(str(getattr(misp_event, item[1]).strftime(EXPORT_DATE_FORMAT)), col2_style)])
|
||||
else :
|
||||
data.append([Paragraph(item[0], col1_style), Paragraph(item[2], col2_style)])
|
||||
|
||||
# Published (Factorized, because too long)
|
||||
item = list_attr_manual[1]
|
||||
data.append([Paragraph(item[0], col1_style), get_published_value(misp_event)])
|
||||
|
||||
# Create styles and set parameters
|
||||
alternate_colors_style = alternate_colors_style_generator(data)
|
||||
lines_style = lines_style_generator(data)
|
||||
|
||||
# Create the table
|
||||
curr_table = Table(data, COL_WIDTHS,
|
||||
rowHeights=(ROW_HEIGHT)) # colWidths='*' does a 100% and share the space automatically
|
||||
|
||||
# Make the table nicer
|
||||
curr_table.setStyle(TableStyle([('TEXTCOLOR', (0, 0), (0, -1), FIRST_COL_FONT_COLOR),
|
||||
('TEXTCOLOR', (1, 0), (-1, -1), SECOND_COL_FONT_COLOR),
|
||||
('FONT', (0, 0), (0, -1), FIRST_COL_FONT),
|
||||
('FONT', (1, 0), (-1, -1), SECOND_COL_FONT),
|
||||
('FONTSIZE', (0, 0), (-1, -1), TEXT_FONT_SIZE),
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
('INNERGRID', (0, 0), (-1, -1), 0.25, colors.black),
|
||||
# ('BOX', (0, 0), (-1, -1), 0.25, colors.black) # Box for all
|
||||
] + alternate_colors_style + lines_style))
|
||||
|
||||
return curr_table
|
||||
|
||||
|
||||
def create_style():
|
||||
sample_style_sheet = getSampleStyleSheet()
|
||||
|
||||
custom_body_style = sample_style_sheet['BodyText']
|
||||
custom_body_style.fontName = 'Helvetica'
|
||||
custom_body_style.fontSize = 9
|
||||
|
||||
# custom_body_style.listAttrs() # Print list of attributes that can be changed
|
||||
# styles.add(ParagraphStyle(name='Justify', alignment=TA_JUSTIFY))
|
||||
|
||||
return custom_body_style
|
||||
|
||||
|
||||
def get_table_styles():
|
||||
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 collect_parts(misp_event: pymisp.MISPEvent):
|
||||
# List of elements/content we want to add
|
||||
flowables = []
|
||||
# Get the list of available styles
|
||||
sample_style_sheet = getSampleStyleSheet()
|
||||
|
||||
# Create own style
|
||||
custom_style = create_style()
|
||||
|
||||
# Create stuff
|
||||
paragraph_1 = Paragraph("A title", sample_style_sheet['Heading1'])
|
||||
paragraph_2 = Paragraph("Some normal body text",sample_style_sheet['BodyText'])
|
||||
paragraph_3 = Paragraph("Dingbat paragraph", sample_style_sheet['BodyText']) # Apply custom style
|
||||
Paragraph("A <b>bold</b> word.<br /> An <i>italic</i> word.", sample_style_sheet['BodyText']) # HTML markup is working too
|
||||
paragraph_1 = Paragraph(misp_event.info, sample_style_sheet['Heading1'])
|
||||
paragraph_2 = Paragraph(str(misp_event.to_json()), custom_style)
|
||||
paragraph_3 = Paragraph("Dingbat <font name=HELVETICA-bold>paragraph</font>",
|
||||
sample_style_sheet['BodyText']) # Apply custom style
|
||||
paragraph_4 = Paragraph("A <b>bold</b> word.<br /> An <i>italic</i> word.",
|
||||
sample_style_sheet['BodyText']) # HTML markup is working too
|
||||
table = create_flowable_table_from_event(misp_event)
|
||||
|
||||
# Add all parts to final PDF
|
||||
flowables.append(paragraph_1)
|
||||
flowables.append(paragraph_2)
|
||||
flowables.append(table)
|
||||
flowables.append(PageBreak())
|
||||
flowables.append(PageBreak())
|
||||
flowables.append(paragraph_2)
|
||||
flowables.append(paragraph_3)
|
||||
|
||||
return flowables
|
||||
|
||||
|
||||
def add_page_number(canvas, doc):
|
||||
canvas.saveState()
|
||||
canvas.setFont('Times-Roman', 10)
|
||||
page_number_text = "%d" % (doc.page)
|
||||
|
||||
canvas.drawCentredString(
|
||||
0.75 * inch,
|
||||
0.75 * inch,
|
||||
page_number_text
|
||||
)
|
||||
|
||||
canvas.restoreState()
|
||||
|
||||
|
||||
def export_flowables_to_pdf(document, pdf_buffer, flowables):
|
||||
# my_doc.build(flowables) # Basic building of the final document
|
||||
|
||||
document.build(
|
||||
flowables,
|
||||
onFirstPage=add_page_number, # Pagination for first page
|
||||
onLaterPages=add_page_number, # Pagination for all other page
|
||||
onFirstPage=add_page_number, # Pagination for first page
|
||||
onLaterPages=add_page_number, # Pagination for all other page
|
||||
)
|
||||
|
||||
|
||||
'''
|
||||
"EXTERNAL" exposed METHODS. Meant to be used outside of this class.
|
||||
'''
|
||||
|
||||
PAGESIZE = (140 * mm, 216 * mm) # width, height
|
||||
BASE_MARGIN = 5 * mm
|
||||
PAGESIZE = (140 * mm, 216 * mm) # width, height
|
||||
BASE_MARGIN = 5 * mm # Create a list here to specify each row separately
|
||||
|
||||
def convert_event_in_pdf_buffer(misp_event):
|
||||
|
||||
def convert_event_in_pdf_buffer(misp_event: pymisp.MISPEvent):
|
||||
# Create a document buffer
|
||||
pdf_buffer = BytesIO()
|
||||
|
||||
# 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)
|
||||
pagesize=PAGESIZE,
|
||||
topMargin=BASE_MARGIN,
|
||||
leftMargin=BASE_MARGIN,
|
||||
rightMargin=BASE_MARGIN,
|
||||
bottomMargin=BASE_MARGIN)
|
||||
|
||||
# Apply standard template
|
||||
# TODO
|
||||
|
@ -128,9 +348,9 @@ def convert_event_in_pdf_buffer(misp_event):
|
|||
export_flowables_to_pdf(curr_document, pdf_buffer, flowables)
|
||||
pdf_value = pdf_buffer.getvalue()
|
||||
|
||||
#TODO : Not sure what to give back ? Buffer ? Buffer.value() ? Base64(buffer.value()) ? ...
|
||||
#pdf_buffer.close()
|
||||
#return pdf_value
|
||||
# TODO : Not sure what to give back ? Buffer ? Buffer.value() ? Base64(buffer.value()) ? ...
|
||||
# pdf_buffer.close()
|
||||
# return pdf_value
|
||||
|
||||
return pdf_buffer
|
||||
|
||||
|
@ -138,35 +358,27 @@ def convert_event_in_pdf_buffer(misp_event):
|
|||
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 register_to_file(pdf_buffer, file_name):
|
||||
pdf_buffer.seek(0)
|
||||
|
||||
with open(file_name, 'wb') as f:
|
||||
f.write(pdf_buffer.read())
|
||||
|
||||
if __name__ == "__main__":
|
||||
pdf_buffer = convert_event_in_pdf_buffer(None)
|
||||
|
||||
register_to_file(pdf_buffer, 'test.pdf')
|
||||
if __name__ == "__main__":
|
||||
# pdf_buffer = convert_event_in_pdf_buffer(None)
|
||||
|
||||
# register_to_file(pdf_buffer, 'test.pdf')
|
||||
get_sample_fonts()
|
||||
|
||||
# get_values_from_buffer(pdf_buffer)
|
||||
# get_base64_from_buffer(pdf_buffer)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
''' In the future ?
|
||||
try:
|
||||
from pymispgalaxies import Clusters
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import unittest
|
||||
import json
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
from pymisp import MISPEvent, MISPSighting, MISPTag, reportlab_generator
|
||||
|
||||
class TestMISPEvent(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.mispevent = MISPEvent()
|
||||
self.test_folder = "reportlab_testfiles/"
|
||||
self.storage_folder = "reportlab_testoutputs/"
|
||||
|
||||
def init_event(self):
|
||||
self.mispevent.info = 'This is a test'
|
||||
self.mispevent.distribution = 1
|
||||
self.mispevent.threat_level_id = 1
|
||||
self.mispevent.analysis = 1
|
||||
self.mispevent.set_date("2017-12-31") # test the set date method
|
||||
|
||||
def test_basic_event(self):
|
||||
self.init_event()
|
||||
reportlab_generator.register_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent), self.storage_folder + "basic_event.pdf")
|
||||
|
||||
def test_event(self):
|
||||
self.init_event()
|
||||
self.mispevent.load_file(self.test_folder + 'to_delete1.json')
|
||||
reportlab_generator.register_to_file(reportlab_generator.convert_event_in_pdf_buffer(self.mispevent),
|
||||
self.storage_folder + "basic_event.pdf")
|
||||
|
||||
# TODO : To modify below this line
|
||||
def test_loadfile(self):
|
||||
self.mispevent.load_file('tests/mispevent_testfiles/event.json')
|
||||
with open('tests/mispevent_testfiles/event.json', 'r') as f:
|
||||
ref_json = json.load(f)
|
||||
self.assertEqual(self.mispevent.to_json(), json.dumps(ref_json, sort_keys=True, indent=2))
|
Loading…
Reference in New Issue