Unverified Commit e1c1c993 by Johannes vom Dorp Committed by GitHub

Merge pull request #1 from fkie-cad/beta

Beta version of report generator
parents f7067821 908d4702
.project
.pydevproject
.idea
# Byte-compiled / optimized / DLL files
__pycache__/
......
[settings]
line_length=120
known_first_party=test
multi_line_output=6
default_stages: [commit, push]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.1.0
hooks:
- id: check-added-large-files
args: [--maxkb=10000]
- id: check-json
- id: check-merge-conflict
- id: check-yaml
- id: end-of-file-fixer
types: [python]
- id: fix-encoding-pragma
args: [--remove]
- id: file-contents-sorter
files: src/unpacker/passwords|.gitignore|_list.txt
- id: flake8
args: [--ignore="E501,W503", --select=W504]
- id: forbid-new-submodules
- id: no-commit-to-branch
- id: pretty-format-json
args: [--autofix]
- id: trailing-whitespace
types: [python]
- repo: https://github.com/pre-commit/mirrors-pylint
rev: v2.2.2
hooks:
- id: pylint
language: system
args: [--rcfile=.pylintrc]
- repo: https://github.com/pre-commit/mirrors-jshint
rev: v2.10.2
hooks:
- id: jshint
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.20
hooks:
- id: isort
[MASTER]
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=test/,src/bin
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=pylint.extensions.bad_builtin,pylint.extensions.mccabe
# Use multiple processes to speed up Pylint.
jobs=2
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality. This option is deprecated
# and it will be removed in Pylint 2.0.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Disable the message, report, category or checker with the given id(s)
disable=missing-docstring,locally-disabled,logging-format-interpolation
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, html
output-format=colorized
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]". This option is deprecated
# and it will be removed in Pylint 2.0.
files-output=no
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
msg-template="{path}:{line}: [{symbol}:{obj}] {msg}"
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set). This supports can work
# with qualified names.
ignored-classes=
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=200
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[BASIC]
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,50}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,50}$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{1,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{1,30}$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,33}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,33}$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{1,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{1,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{1,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{1,30}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[ELIF]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=optparse
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
[DESIGN]
# McCabe complexity cyclomatic threshold
max-complexity=7
# Maximum number of arguments for function / method
max-args=7
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=0
# Maximum number of public methods for a class (see R0904).
max-public-methods=40
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception
language: python
python:
- "3.5"
- "3.6"
# command to install dependencies
install:
- "pip install -r requirements.txt"
- "pip install codecov"
# command to run tests
script: "pytest"
after_success:
- codecov
\ No newline at end of file
FROM phusion/baseimage:0.11
WORKDIR /opt/app
COPY . /opt/app
RUN install_clean git python3 python3-pip python3-wheel python3-setuptools texlive-latex-base texlive-latex-extra lmodern
RUN pip3 install -r requirements.txt
RUN apt-get remove -y python3-pip
ENTRYPOINT ["./docker_entry.py"]
#!/usr/bin/env python3
'''
fact_pdf_report
Copyright (C) 2015-2019 Fraunhofer FKIE
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
import json
import shutil
from pathlib import Path
from tempfile import TemporaryDirectory
from pdf_generator.generator import compile_pdf, create_templates
def get_data():
return json.loads(Path('/tmp', 'interface', 'data', 'analysis.json').read_text()), json.loads(Path('/tmp', 'interface', 'data', 'meta.json').read_text())
def move_pdf_report(pdf_path):
shutil.move(str(pdf_path.absolute()), str(Path('/tmp', 'interface', 'pdf', pdf_path.name)))
def main():
analysis, meta_data = get_data()
with TemporaryDirectory() as tmp_dir:
create_templates(analysis, meta_data, tmp_dir)
target_path = compile_pdf(meta_data, tmp_dir)
move_pdf_report(target_path)
return 0
if __name__ == '__main__':
exit(main())
import os
import shutil
from pathlib import Path
from common_helper_process import execute_shell_command
from pdf_generator.tex_generation.template_engine import (
LOGO_FILE, MAIN_TEMPLATE, META_TEMPLATE, PLUGIN_TEMPLATE_BLUEPRINT, TemplateEngine
)
PDF_NAME = Path(MAIN_TEMPLATE).with_suffix('.pdf').name
def execute_latex(tmp_dir):
current_dir = os.getcwd()
os.chdir(tmp_dir)
execute_shell_command('env buf_size=1000000 pdflatex {}'.format(MAIN_TEMPLATE))
os.chdir(current_dir)
def copy_fact_image(target):
shutil.copy(str(Path(__file__).parent / 'templates' / LOGO_FILE), str(Path(target) / LOGO_FILE))
def render_analysis_templates(engine, analysis):
return [
(PLUGIN_TEMPLATE_BLUEPRINT.format(analysis_plugin), engine.render_analysis_template(analysis_plugin, analysis[analysis_plugin])) for analysis_plugin in analysis
]
def create_report_filename(meta_data):
unsafe_name = '{}_analysis_report.pdf'.format(meta_data['device_name'])
safer_name = unsafe_name.replace(' ', '_').replace('/', '__')
return safer_name.encode('latin-1', errors='ignore').decode('latin-1')
def compile_pdf(meta_data, tmp_dir):
copy_fact_image(tmp_dir)
execute_latex(tmp_dir)
target_path = Path(tmp_dir, create_report_filename(meta_data))
shutil.move(str(Path(tmp_dir, PDF_NAME)), str(target_path))
return target_path
def create_templates(analysis, meta_data, tmp_dir):
engine = TemplateEngine(tmp_dir=tmp_dir)
Path(tmp_dir, MAIN_TEMPLATE).write_text(engine.render_main_template(analysis=analysis, meta_data=meta_data))
Path(tmp_dir, META_TEMPLATE).write_text(engine.render_meta_template(meta_data))
for filename, rendered_template in render_analysis_templates(engine=engine, analysis=analysis):
Path(tmp_dir, filename).write_text(rendered_template)
\subsection*{Binwalk (v. \VAR{selected_analysis['plugin_version']})}
{\fontfamily{lmss}\selectfont
\begin{longtable}{|p{3cm}|p{11.5cm}|}
\hline
\multicolumn{2}{|p{14.5cm}|}{Signature Analysis:} \\
\multicolumn{2}{|p{14.5cm}|}{} \\
\multicolumn{2}{|p{14.5cm}|}{\VAR{selected_analysis['signature_analysis'] | filter_chars}} \\
\hline
Entropy Graph & \includegraphics[scale = 0.7]{\VAR{selected_analysis['entropy_analysis_graph'] | base64_to_png('entropy_analysis_graph', tmp_dir)}} \\
\hline
\BLOCK{if selected_analysis['summary']}
\multicolumn{2}{|p{14.5cm}|}{Summary:} \\
\multicolumn{2}{|p{14.5cm}|}{} \\
\BLOCK{for data in selected_analysis['summary']}
\multicolumn{2}{|p{14.5cm}|}{\VAR{data | filter_chars}} \\
\BLOCK{endfor}
\hline
\BLOCK{endif}
\end{longtable}
}
\ No newline at end of file
\subsection*{Exploit Mitigation (v. \VAR{selected_analysis['plugin_version']})}
{\fontfamily{lmss}\selectfont
\begin{longtable}{|p{3cm}|p{11.5cm}|}
\hline
\BLOCK{if selected_analysis['summary']}
NX
&
\BLOCK{for selected_summary in selected_analysis['summary']}
\BLOCK{if selected_summary | contains('NX')}
\VAR{selected_summary | filter_chars} (\VAR{ selected_analysis['summary'][selected_summary] | elements_count })
\BLOCK{endif}
\BLOCK{endfor} \\
\hline
Canary
&
\BLOCK{for selected_summary in selected_analysis['summary']}
\BLOCK{if selected_summary | contains('Canary')}
\VAR{selected_summary | filter_chars} (\VAR{ selected_analysis['summary'][selected_summary] | elements_count })
\BLOCK{endif}
\BLOCK{endfor} \\
\hline
PIE
&
\BLOCK{for selected_summary in selected_analysis['summary']}
\BLOCK{if selected_summary | contains('PIE')}
\VAR{selected_summary | filter_chars} (\VAR{ selected_analysis['summary'][selected_summary] | elements_count })
\BLOCK{endif}
\BLOCK{endfor} \\
\hline
RELRO
&
\BLOCK{for selected_summary in selected_analysis['summary']}
\BLOCK{if selected_summary | contains('RELRO')}
\VAR{selected_summary | filter_chars} (\VAR{ selected_analysis['summary'][selected_summary] | elements_count })
\BLOCK{endif}
\BLOCK{endfor} \\
\hline
\BLOCK{endif}
\end{longtable}
}
\ No newline at end of file
\subsection*{Hashes (v. \VAR{selected_analysis['plugin_version']})}
{\fontfamily{lmss}\selectfont
\begin{tabular}{|p{3cm}|p{11.5cm}|}
\hline
md5 & \VAR{selected_analysis['md5']}\\
\hline
ripemd160 & \VAR{selected_analysis['ripemd160']}\\
\hline
sha1 & \VAR{selected_analysis['sha1']}\\
\hline
sha256 & \VAR{selected_analysis['sha256']}\\
\hline
sha512 & \VAR{selected_analysis['sha512'] | split_hash}\\
\hline
\end{tabular}
}
\ No newline at end of file
\subsection*{File Type (v. \VAR{selected_analysis['plugin_version']})}
{\fontfamily{lmss}\selectfont
\begin{longtable}{|p{3cm}|p{11.5cm}|}
\hline
File Type & \VAR{selected_analysis['full'] | filter_chars} \\
\hline
MIME & \VAR{selected_analysis['mime'] | filter_chars} \\
\hline
\BLOCK{if selected_analysis['summary']}
Containing Files
\BLOCK{for selected_summary in selected_analysis['summary']}
& \VAR{ selected_summary | filter_chars } (\VAR{ selected_analysis['summary'][selected_summary] | elements_count }) \\
\BLOCK{endfor}
\hline
\BLOCK{endif}
\end{longtable}
}
\ No newline at end of file
\subsection*{\VAR{plugin_name | plugin_name | filter_chars} (v. \VAR{selected_analysis['plugin_version'] | filter_chars})}
{\fontfamily{lmss}\selectfont
\begin{longtable}{|p{3cm}|p{11.5cm}|}
\hline
Time of Analysis & \VAR{selected_analysis['analysis_date'] | nice_unix_time} \\
\hline
Plugin Version & \VAR{selected_analysis['plugin_version']} \\
\hline
\BLOCK{if selected_analysis['summary']}
Summary
\BLOCK{for selected_summary in selected_analysis['summary']}
& \VAR{selected_summary | filter_chars} \\
\BLOCK{endfor}
\hline
\BLOCK{endif}
\end{longtable}
}
\ No newline at end of file
\subsection*{IPs and URIs (v. \VAR{selected_analysis['plugin_version']})}
{\fontfamily{lmss}\selectfont
\begin{longtable}{|p{3cm}|p{11.5cm}|}
\hline
IPs v4
\BLOCK{for ip in selected_analysis['ips_v4'] | check_list | filter_list}
& \VAR{ip} \\
\BLOCK{endfor}
\hline
IPs v6
\BLOCK{for ip in selected_analysis['ips_v6'] | check_list | filter_list}
& \VAR{ip} \\
\BLOCK{endfor}
\hline
URIs
\BLOCK{for uri in selected_analysis['uris'] | check_list | filter_list}
& \VAR{uri} \\
\BLOCK{endfor}
\hline
\BLOCK{if selected_analysis['summary']}
\multicolumn{2}{|p{14.5cm}|}{Summary:} \\
\multicolumn{2}{|p{14.5cm}|}{} \\
\BLOCK{for data in selected_analysis['summary']}
\multicolumn{2}{|p{14.5cm}|}{\VAR{data | filter_chars}} \\
\BLOCK{endfor}
\hline
\BLOCK{endif}
\end{longtable}
}
\ No newline at end of file
\documentclass{article}
\usepackage[english]{babel}
\usepackage[utf8]{inputenc}
\usepackage{amsmath}
\usepackage{graphicx}
\usepackage{lmodern}
\usepackage[a4paper, total={6in, 8in}]{geometry}
\usepackage{longtable}
\usepackage{sectsty}
\allsectionsfont{\sffamily}
\begin{document}
\begin{titlepage}
\newcommand{\HRule}{\rule{\linewidth}{0.5mm}}
\center
\includegraphics[width = 13cm]{fact_logo.png}\\[3cm]
\HRule \\[0.4cm]
{\fontfamily{lmss}\selectfont
{\Large \bfseries Firmware Analysis of \VAR{meta_data['hid'] | filter_chars }}\\[0.4cm]
}
\HRule \\[1.5cm]
\vspace{3.0cm}
{\fontfamily{lmss}\selectfont
\input{meta.tex}
}
\vfill
\end{titlepage}
\BLOCK{for current_analysis in analysis}
\input{\VAR{current_analysis}.tex}
\newpage
\BLOCK{endfor}
\end{document}
\subsection*{Malware (v. \VAR{selected_analysis['plugin_version']})}
{\fontfamily{lmss}\selectfont
\begin{longtable}{|p{3cm}|p{11.5cm}|}
\hline
MD5 & \VAR{selected_analysis['md5']} \\
\hline
System Version & \VAR{selected_analysis['system_version']} \\
\hline
Scanners Number & \VAR{selected_analysis['number_of_scanners']} \\
\hline
Positives & \VAR{selected_analysis['positives']} \\
\hline
Scanners
\BLOCK{for scanner in selected_analysis['scanners']}
& \VAR{scanner} \\
\BLOCK{endfor}
\hline
Scanns
\BLOCK{for scan in selected_analysis['scans']}
\BLOCK{for value in selected_analysis['scans'][scan]}
& \VAR{scan} :: \VAR{value}:\VAR{selected_analysis['scans'][scan][value]} \\
\BLOCK{endfor}
\BLOCK{endfor}
\hline
\BLOCK{if selected_analysis['summary']}
Summary
\BLOCK{for selected_summary in selected_analysis['summary']}
& \VAR{selected_summary | filter_chars} \\
\BLOCK{endfor}
\hline
\BLOCK{endif}
\end{longtable}
}
\ No newline at end of file
\begin{tabular}{|p{3cm}|p{11.5cm}|}
\hline
HID & \VAR{meta_data['hid'] | filter_chars}\\
\hline
Device Name & \VAR{meta_data['device_name'] | filter_chars}\\
\hline
Vendor & \VAR{meta_data['vendor'] | filter_chars}\\
\hline
Device Class & \VAR{meta_data['device_class'] | filter_chars}\\
\hline
Version & \VAR{meta_data['version'] | filter_chars}\\
\hline
Release Date & \VAR{meta_data['release_date']}\\
\hline
Size & \VAR{meta_data['size'] | number_format}\\
\hline
\end{tabular}
\ No newline at end of file
\subsection*{String Stats (v. \VAR{selected_analysis['plugin_version']})}
{\fontfamily{lmss}\selectfont
\begin{tabular}{|p{3cm}|p{11.5cm}|}
\hline
String Count & \VAR{selected_analysis['strings'] | elements_count}\\
\hline
\end{tabular}
}
\ No newline at end of file
\subsection*{String Eval Stats (v. \VAR{selected_analysis['plugin_version']})}
{\fontfamily{lmss}\selectfont
\begin{tabular}{|p{3cm}|p{11.5cm}|}
\hline
String Count & \VAR{selected_analysis['string_eval'] | elements_count}\\
\hline
\end{tabular}
}
\ No newline at end of file
\subsection*{Unpacker (v. \VAR{selected_analysis['plugin_version']})}
{\fontfamily{lmss}\selectfont
\begin{longtable}{|p{3cm}|p{11.5cm}|}
\hline
Plugin & \VAR{selected_analysis['plugin_used'] | filter_chars} \\
\hline
Extracted & \VAR{selected_analysis['number_of_unpacked_files']} \\
\hline
\BLOCK{if selected_analysis['output']}
\multicolumn{2}{|p{14.5cm}|}{Output:}\\
\multicolumn{2}{|p{14.5cm}|}{}\\
\multicolumn{2}{|p{14.5cm}|}{\VAR{selected_analysis['output'] | split_output_lines | filter_chars }} \\
\hline
\BLOCK{endif}
Entropy & \VAR{selected_analysis['entropy'] | nice_number} \\
\hline
\end{longtable}
}
\ No newline at end of file
Presenting: \VAR{selected_analysis}
render_test.tex
\ No newline at end of file
render_test.tex
\ No newline at end of file
Test \VAR{meta_data} - \VAR{analysis}
\ No newline at end of file
import logging
from base64 import decodebytes
from collections import OrderedDict
from contextlib import suppress
from pathlib import Path
from time import localtime, strftime
import jinja2
from common_helper_files import human_readable_file_size
GENERIC_TEMPLATE = 'generic.tex'
MAIN_TEMPLATE = 'main.tex'
META_TEMPLATE = 'meta.tex'
PLUGIN_TEMPLATE_BLUEPRINT = '{}.tex'
LOGO_FILE = 'fact_logo.png'
def render_number_as_size(number, verbose=True):
if not isinstance(number, (int, float)):
return 'not available'
if verbose:
return '{} ({})'.format(human_readable_file_size(int(number)), format(number, ',d') + ' Byte')
return human_readable_file_size(int(number))
def render_unix_time(unix_time_stamp):
if not isinstance(unix_time_stamp, (int, float)):
return 'not available'
return strftime('%Y-%m-%d %H:%M:%S', localtime(unix_time_stamp))
def render_number_as_string(number):
if isinstance(number, int):
return '{:,}'.format(number)
if isinstance(number, float):
return '{:,.2f}'.format(number)
if isinstance(number, str):
with suppress(ValueError):
return str(int(number))
return 'not available'
def replace_special_characters(data):
latex_character_escapes = OrderedDict()
latex_character_escapes['\\'] = ''
latex_character_escapes['\''] = ''
latex_character_escapes['$'] = '\\$'
latex_character_escapes['('] = '$($'
latex_character_escapes[')'] = '$)$'
latex_character_escapes['['] = '$[$'
latex_character_escapes[']'] = '$]$'
latex_character_escapes['#'] = '\\#'
latex_character_escapes['%'] = '\\%'
latex_character_escapes['&'] = '\\&'
latex_character_escapes['_'] = '\\_'
latex_character_escapes['{'] = '\\{'
latex_character_escapes['}'] = '\\}'
latex_character_escapes['^'] = '\\textasciicircum{}'
latex_character_escapes['~'] = '\\textasciitilde{}'
latex_character_escapes['>'] = '\\textgreater{}'
latex_character_escapes['<'] = '\\textless{}'
latex_character_escapes['\n'] = '\\newline '
for character, replacement in latex_character_escapes.items():
if character in data:
data = data.replace(character, replacement)
return data
def decode_base64_to_file(base64_string, filename, directory, suffix='png'):
file_path = Path(directory, '{}.{}'.format(filename, suffix))
file_path.write_bytes(decodebytes(base64_string.encode('utf-8')))
return str(file_path)
def replace_characters_in_list(list_of_strings):
return [
replace_special_characters(item) for item in list_of_strings
]
def split_hash_string(hash_string, max_length=61):
if len(hash_string) > max_length:
hash_string = '{}\n{}'.format(hash_string[:max_length], hash_string[max_length:])
return hash_string
def split_long_lines(multiline_string, max_length=92):
def evaluate_split(line):
return line if len(line) <= max_length else '{}\n{}'.format(line[:max_length], line[max_length:])
return ''.join(
evaluate_split(line) for line in multiline_string.splitlines(keepends=True)
)
def item_contains_string(item, string):
if not isinstance(item, str):
return False
return string in item
def create_jinja_environment(templates_to_use='default'):
template_directory = Path(Path(__file__).parent.parent, 'templates', templates_to_use)
environment = jinja2.Environment(
block_start_string=r'\BLOCK{',
block_end_string='}',
variable_start_string=r'\VAR{',
variable_end_string='}',
comment_start_string=r'\#{',
comment_end_string='}',
line_statement_prefix='%%',
line_comment_prefix='%#',
trim_blocks=True,
autoescape=False,
loader=jinja2.FileSystemLoader(str(template_directory))
)
_add_filters_to_jinja(environment)
return environment
def plugin_name(name):
return ' '.join((part.title() for part in name.split('_')))
def _add_filters_to_jinja(environment):
environment.filters['number_format'] = render_number_as_size
environment.filters['nice_unix_time'] = render_unix_time
environment.filters['nice_number'] = render_number_as_string
environment.filters['filter_chars'] = replace_special_characters
environment.filters['elements_count'] = len
environment.filters['base64_to_png'] = decode_base64_to_file
environment.filters['check_list'] = lambda x: x if x else ['list is empty']
environment.filters['plugin_name'] = plugin_name
environment.filters['filter_list'] = replace_characters_in_list
environment.filters['split_hash'] = split_hash_string
environment.filters['split_output_lines'] = split_long_lines
environment.filters['contains'] = item_contains_string
class TemplateEngine:
def __init__(self, template_folder=None, tmp_dir=None):
self._environment = create_jinja_environment(template_folder if template_folder else 'default')
self._tmp_dir = tmp_dir
def render_main_template(self, analysis, meta_data):
template = self._environment.get_template(MAIN_TEMPLATE)
return template.render(analysis=analysis, meta_data=meta_data)
def render_meta_template(self, meta_data):
template = self._environment.get_template(META_TEMPLATE)
return template.render(meta_data=meta_data)
def render_analysis_template(self, plugin, analysis):
try:
template = self._environment.get_template(PLUGIN_TEMPLATE_BLUEPRINT.format(plugin))
except jinja2.TemplateNotFound:
logging.warning('Falling back on generic template for {}'.format(plugin))
template = self._environment.get_template(GENERIC_TEMPLATE)
return template.render(plugin_name=plugin, selected_analysis=analysis, tmp_dir=self._tmp_dir)
[pytest]
addopts = -v
pep8ignore =
*.py E501
pytest
pytest-cov
pre-commit
requests
jinja2
git+https://github.com/fkie-cad/common_helper_files.git
git+https://github.com/fkie-cad/common_helper_process.git
TEST_DICT = {
"firmware": {
"analysis": {
"binwalk": {
"analysis_date": 1548333205.871766,
"entropy_analysis_graph": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzs3XlclWX+//H3AWUTQc0EJBRzxVLcDatRi8JlTGtqTEnFypbRUhnNpdzHZWbMsHRy0kQrTG3RSs0y0mbKhVywTHNXTAVSY3NDOdfvj76e35xAxQQOnvv1fDzuh97Xfd33/bnOwcPbezs2Y4wRAAAALMPD1QUAAACgbBEAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQADl2oIFC2Sz2S47bdy48Zq2t2rVKo0fP750igWAG0QFVxcAAMUxceJE1alTp1B7vXr1rmk7q1at0uzZswmBACyNAAjghtC5c2e1atWqTPd58eJF2e12eXl5lel+AaC0cQoYwA3v0KFDstlsmj59ut544w3VrVtX3t7eat26tb799ltHv7i4OM2ePVuSnE4j/3YbCQkJjm3s3LlTkpSZmaknnnhCQUFB8vHxUWRkpBYuXHjZOl555RXVrl1bvr6+at++vXbs2OHol5iYKJvNpm3bthUay5QpU+Tp6amjR4+W+OsEAJdwBBDADSE7O1snTpxwarPZbLrpppsc84sWLVJubq6efvpp2Ww2/eMf/9BDDz2kAwcOqGLFinr66ad17NgxrVmzRm+//XaR+0lMTNS5c+f01FNPydvbW9WqVdPZs2fVoUMH7du3T4MGDVKdOnX03nvvKS4uTllZWRo8eLDTNt566y3l5uZq4MCBOnfunGbOnKl77rlH33//vYKCgvTwww9r4MCBSkpKUvPmzZ3WTUpKUocOHRQaGlpCrxwAFMEAQDmWmJhoJBU5eXt7G2OMOXjwoJFkbrrpJnPq1CnHuh999JGRZD755BNH28CBA01RH32XthEQEGAyMzOdliUkJBhJ5p133nG05efnm6ioKOPv729ycnKctuHr62t++uknR99NmzYZSWbo0KGOtl69epmaNWuagoICR9vWrVuNJJOYmPg7Xy0AKB5OAQO4IcyePVtr1qxxmj799FOnPj179lTVqlUd83fffbck6cCBA8Xez5/+9CfdfPPNTm2rVq1ScHCwevXq5WirWLGinn/+eeXl5emrr75y6t+jRw+nI3ht2rRR27ZttWrVKkdb3759dezYMa1du9bRlpSUJF9fX/3pT38qdr0A8HtwChjADaFNmzZXvQmkVq1aTvOXwuAvv/xS7P0Udafx4cOHVb9+fXl4OP+fOSIiwrH8f9WvX7/QNho0aKClS5c65u+77z6FhIQoKSlJ9957r+x2u9599111795dlStXLna9APB7cAQQgNvw9PQsst0YU+xt+Pr6llQ5V+Tp6anevXvrgw8+0Llz57R27VodO3ZMjz32WJnsH4C1EQABWMqlu36vRe3atbV3717Z7Xan9h9//NGx/H/t3bu30Db27Nmj8PBwp7a+ffsqJydHn3zyiZKSknTzzTcrJibmmusDgGtFAARgKZUqVZIkZWVlFXudLl26KD09XUuWLHG0Xbx4Ua+99pr8/f3Vvn17p/7Lly93eoxLSkqKNm3apM6dOzv1a9q0qZo2bap58+bpgw8+0KOPPqoKFbgyB0Dp45MGwA3h008/dRxx+1/t2rUrdG3elbRs2VKS9PzzzysmJkaenp569NFHr7jOU089pX//+9+Ki4vTli1bFB4ervfff1/ffPONEhISCl2zV69ePd1111169tlndf78eSUkJOimm27SCy+8UGjbffv21bBhwySJ078AygwBEMANYezYsUW2JyYmqkOHDsXezkMPPaTnnntOixcv1jvvvCNjzFUDoK+vr9atW6eRI0dq4cKFysnJUcOGDZWYmKi4uLhC/fv27SsPDw8lJCQoMzNTbdq00axZsxQSElKob2xsrEaMGKG6deuqTZs2xR4HAFwPm7mWq6MBAJd16NAh1alTR//85z8dR/Wu5sSJEwoJCdHYsWM1ZsyYUq4QAH7FNYAA4EILFixQQUGB+vTp4+pSAFgIp4ABwAW+/PJL7dy5U5MnT1aPHj0K3SEMAKWJAAgALjBx4kStX79ed955p1577TVXlwPAYrgGEAAAwGK4BhAAAMBiCIAAAAAWQwAEAACwGG4CuQ52u13Hjh1T5cqVf9f3iwIAgLJnjFFubq5q1qx5Td8k5E4IgNfh2LFjCgsLc3UZAADgdzhy5IhuueUWV5fhEtaMvSXkt9//CQAAbhxW/j1OALwOnPYFAODGZeXf4wRAAAAAiyEAAgAAWAwBEAAAwGIIgAAAABZDAAQAALAYAiAAAIDFEAABAAAshgAIAABgMQRAAAAAiyEAAgAAWAwBEAAAwGIIgAAAABZTwdUF4DKekuQvKU/SG26wnxutltJihTEWF68FgCv4VlKwpHRJrV1cizsiAJZX/pIC3Gg/xVGeaiktVhhjcfFaALiCYEm3uLoIN8YpYAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAItxmwD4n//8R926dVPNmjVls9m0fPnyq66zbt06tWjRQt7e3qpXr54WLFhQ+oUCAAC4mNsEwNOnTysyMlKzZ88uVv+DBw+qa9eu6tixo1JTUzVkyBA9+eST+uyzz0q5UgAAANeq4OoCSkrnzp3VuXPnYvefM2eO6tSpo5dfflmSFBERoa+//lqvvPKKYmJiSqtMAAAAl3ObI4DXasOGDYqOjnZqi4mJ0YYNG1xUEQAAQNlwmyOA1yo9PV1BQUFObUFBQcrJydHZs2fl6+tbaJ3z58/r/PnzjvmcnJxSrxMAAKCkWfYI4O8xdepUBQYGOqawsDBXlwQAAHDNLBsAg4ODlZGR4dSWkZGhgICAIo/+SdKoUaOUnZ3tmI4cOVIWpQIAAJQoy54CjoqK0qpVq5za1qxZo6ioqMuu4+3tLW9v79IuDQAAoFS5zRHAvLw8paamKjU1VdKvj3lJTU1VWlqapF+P3vXt29fR/5lnntGBAwf0wgsv6Mcff9S//vUvLV26VEOHDnVJ/QAAAGXFbQLg5s2b1bx5czVv3lySFB8fr+bNm2vs2LGSpOPHjzvCoCTVqVNHK1eu1Jo1axQZGamXX35Z8+bN4xEwAADA7bnNKeAOHTrIGHPZ5UV9y0eHDh20bdu2UqwKAACg/HGbI4AAAAAoHgIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALMatAuDs2bMVHh4uHx8ftW3bVikpKVfsn5CQoIYNG8rX11dhYWEaOnSozp07V0bVAgAAuIbbBMAlS5YoPj5e48aN09atWxUZGamYmBhlZmYW2X/RokUaOXKkxo0bp127dunNN9/UkiVLNHr06DKuHAAAoGy5TQCcMWOGBgwYoP79+6tx48aaM2eO/Pz8NH/+/CL7r1+/Xnfeead69+6t8PBw3X///erVq9dVjxoCAADc6NwiAObn52vLli2Kjo52tHl4eCg6OlobNmwocp127dppy5YtjsB34MABrVq1Sl26dLnsfs6fP6+cnBynCQAA4EZTwdUFlIQTJ06ooKBAQUFBTu1BQUH68ccfi1ynd+/eOnHihO666y4ZY3Tx4kU988wzVzwFPHXqVE2YMKFEawcAAChrbnEE8PdYt26dpkyZon/961/aunWrPvzwQ61cuVKTJk267DqjRo1Sdna2Yzpy5EgZVgwAAFAy3OIIYPXq1eXp6amMjAyn9oyMDAUHBxe5zpgxY9SnTx89+eSTkqQmTZro9OnTeuqpp/Tiiy/Kw6NwNvb29pa3t3fJDwAAAKAMucURQC8vL7Vs2VLJycmONrvdruTkZEVFRRW5zpkzZwqFPE9PT0mSMab0igUAAHAxtzgCKEnx8fHq16+fWrVqpTZt2ighIUGnT59W//79JUl9+/ZVaGiopk6dKknq1q2bZsyYoebNm6tt27bat2+fxowZo27dujmCIAAAgDtymwDYs2dP/fzzzxo7dqzS09PVrFkzrV692nFjSFpamtMRv5deekk2m00vvfSSjh49qptvvlndunXT5MmTXTUEAACAMmEznO/83XJychQYGFg6G4+XFCApR9KM0tlFme7nRqultFhhjMXFawHgCo5IukXST5LCSmkf2dnZCggIKKWtl29ucQ0gAAAAio8ACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAW4/IA2L59e7311ls6e/asq0sBAACwBJcHwObNm2vYsGEKDg7WgAEDtHHjRleXBAAA4NZcHgATEhJ07NgxJSYmKjMzU3/4wx/UuHFjTZ8+XRkZGa4uDwAAwO24PABKUoUKFfTQQw/po48+0k8//aTevXtrzJgxCgsLU48ePfTll1+6ukQAAAC3US4C4CUpKSkaN26cXn75ZdWoUUOjRo1S9erV9cc//lHDhg276vqzZ89WeHi4fHx81LZtW6WkpFyxf1ZWlgYOHKiQkBB5e3urQYMGWrVqVUkNBwAAoFyq4OoCMjMz9fbbbysxMVF79+5Vt27d9O677yomJkY2m02SFBcXp06dOmn69OmX3c6SJUsUHx+vOXPmqG3btkpISFBMTIx2796tGjVqFOqfn5+v++67TzVq1ND777+v0NBQHT58WFWqVCm1sQIAAJQHLg+At9xyi+rWravHH39ccXFxuvnmmwv1adq0qVq3bn3F7cyYMUMDBgxQ//79JUlz5szRypUrNX/+fI0cObJQ//nz5+vUqVNav369KlasKEkK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/HcOAFDIpdB36RcpcKO59LPbvHlz1ahRw8XVlD8EQABAIZdO93LaFzeqSz+7xhjVrFmT/8z8Bv+yAQCA28rPz5enp6e8vb1dXUq5QgAEAFjOunXrZLPZlJWV5epSCjl06JBsNptSU1Mv22fBggWqUqVKGVbl2v2WBJvN5uoSyhUCIADAbVwKdpebOnbsKElq166djh8/rsDAQBdX/Pv07NlTe/bscXUZxRIeHq6EhARXl4Hf4C5gAIDbuBTsfuvjjz/WM888o7/85S+SJC8vL5fc3Zyfny8vL6/r3o6vr698fX1LoCJYFUcAAQBu41JneKuIAAAgAElEQVSw+9/pl19+0bBhwzR69Gg98sgjkgqfAr50anP58uWqX7++fHx8FBMToyNHjlxxfyNGjFCDBg3k5+enW2+9VWPGjNGFCxccy8ePH69mzZpp3rx5qlOnjnx8fCT9+qy6f/zjH6pXr568vb1Vq1atQs+rO3DggDp27Cg/Pz9FRkZqw4YNjmX/eyp2z549stls+vHHH53Wf+WVV1S3bl3H/I4dO9S5c2f5+/srKChIffr00YkTJ644vgULFqhWrVry8/PTgw8+qJMnTzot379/v7p3766goCD5+/urdevW+uKLLxzLO3TooMOHD2vo0KGOo7CSdPLkSfXq1UuhoaHy8/NTkyZN9O67716xFpQsAiAAwG1lZWWpe/fu6tChgyZNmnTFvmfOnNHkyZP11ltv6ZtvvlFWVpYeffTRK65TuXJlLViwQDt37tTMmTM1d+5cvfLKK0599u3bpw8++EAffvih47q+UaNGadq0aRozZox27typRYsWKSgoyGm9F198UcOGDVNqaqoaNGigXr166eLFi4VqaNCggVq1aqWkJOfvy0hKSlLv3r0dr8M999yj5s2ba/PmzVq9erUyMjL05z//+bJj27Rpk5544gkNGjRIqamp6tixo/72t7859cnLy1OXLl2UnJysbdu2qVOnTurWrZvS0tIkSR9++KFuueUWTZw4UcePH3ccnT137pxatmyplStXaseOHXrqqafUp08fpaSkXPH1RsnhFDAAoNhavdFK6XnpZb7fYP9gbX5q8zWtY7fb1bt3b1WoUEFJSUlXvQngwoULmjVrltq2bStJWrhwoSIiIpSSkqI2bdoUuc5LL73k+Ht4eLiGDRumxYsX64UXXnC05+fn66233tLNN98sScrNzdXMmTM1a9Ys9evXT5JUt25d3XXXXU7bHjZsmLp27SpJmjBhgm677Tbt27dPjRo1KlRHbGysZs2a5Qi5e/bs0ZYtW/TOO+9IkmbNmqXmzZtrypQpjnXmz5+vsLAw7dmzRw0aNCi0zZkzZ6pTp06OsTRo0EDr16/X6tWrHX0iIyMVGRnpmJ80aZKWLVumjz/+WIMGDVK1atXk6empypUrO51yDw0N1bBhwxzzzz33nD777DMtXbr0sq81ShYBEABQbOl56Tqae9TVZRTL6NGjtWHDBqWkpKhy5cpX7V+hQgW1bt3aMd+oUSNVqVJFu3btumwoWbJkiV599VXt379feXl5unjxogICApz61K5d2xH+JGnXrl06f/687r333ivW07RpU8ffQ0JCJEmZmZlFBsBHH31Uw4YN08aNG3XHHXcoKSlJLVq0cPTdvn271q5dW+Sz8Pbv319kANy1a1eh74GOiopyCoB5eXkaP368Vq5cqePHj+vixYs6e/as4wjg5RQUFGjKlClaunSpjh49qvz8fJ0/f15+fn5XXA8lhwAIACi2YH/XfC3cte538eLFmj59ulauXKn69euXSk0bNmxQbGysJkyYoJiYGAUGBmrx4sV6+eWXnfpVqlTJab64N29UrFjR8fdLRy/tdnuRfYODg3XPPfdo0aJFuuOOO7Ro0SI9++yzjuV5eXnq1q2b/v73vxda91K4/D2GDRumNWvWaPr06apXr558fX318MMPKz8//4rr/fOf/9TMmTOVkJCgJk2aqFKlShoyZMhV10PJIQACAIrtWk/DukJqaqqeeOIJTZs2TTExMcVe7+LFi9q8ebPjaN/u3buVlZWliIiIIvuvX79etWvX1osvvuhoO3z48FX3U79+ffn6+io5OVlPPvlkseu7mtjYWL3wwgvq1auXDhw44HT9YosWLfTBBx8oPDxcFSoU71d/RESENm3a5NS2ceNGp/lvvvlGcXFxjiOFeXl5OnTokFMfLy8vFRQUFFqve/fueuyxxyT9Gmz37Nmjxo0bF6s2XD9uAgEAuI0TJ06oR48e6tChgx577DGlp6c7TT///PNl161YsaKee+45bdq0SVu2bFFcXJzuuOOOy57+rV+/vtLS0rR48WLt379fr776qpYtW3bVGn18fDRixAi98MILeuutt7R//35t3LhRb7755u8etyQ99NBDys3N1bPPPquOHTuqZs2ajmUDBw7UqVOn1KtXL3377bfav3+/PvvsM/Xv379QOLvk+eef1+rVqzV9+nTt3btXs2bNcjr9e+k1uHRzy/bt29W7d+9CRynDw8P1n//8R0ePHnXcdVy/fn2tWbNG69ev165du/T0008rIyPjusaPa0MABAC4jZUrV+rw4cNatWqVQkJCCk3/e43fb/n5+WnEiBHq3bu37rzzTvn7+2vJkiWX7f/AAw9o6NChGjRokJo1a6b169drzJgxxapzzJgx+utf/6qxY8cqIiJCPXv2VGZm5jWP939VrlxZ3bp10/bt2xUbG+u0rGbNmvrmm29UUFCg+++/X02aNNGQIUNUpUqVy37f8x133KG5c+dq5syZioyM1Oeff+5004skzZgxQ1WrVlW7du3UrVs3xcTEqEWLFk59Jk6cqEOHDqlu3bqOayFfeukltWjRQjExMerQoYOCg4PVo0eP6xo/ro3NGGNcXcSNKicnp/SeIh8vKUBSjqQZpbOLMt3PjVZLabHCGIuL16Jca9iwoZKSkhQREWGJC/MXLFigIUOGlMuvhsPvc+bMGe3atUujR4/WyZMndfjw4ULPPczOzi50045VcAQQAADAYgiAAAAAFkMABABYXlxcHKd/YSkEQAAAAIshAAIAAFgMARAAAMBiCIAAAAAWQwAEAACwGAIgAACAxRAAAQAohnXr1slms13342LeeOMNhYWFycPDQwkJCb97O3FxceX269PGjx+vZs2aXbFPhw4dNGTIkDKqCL9FAAQAuJW4uDjZbLZCU6dOnVxdmnJycjRo0CCNGDFCR48e1VNPPVWoz+XqvzQtXLhQkjRz5kwtWLCgjEdQcj788ENNmjTJ1WVYVgVXFwAAQEnr1KmTEhMTndq8vb1dVM3/l5aWpgsXLqhr164KCQkpss/MmTM1bdq0Qu19+vTRvn371LVrV0kqve+iv4r8/Hx5eXld93aqVatWAtXg9+IIIADA7Xh7eys4ONhpqlq1qmO5zWbTvHnz9OCDD8rPz0/169fXxx9/7LSNVatWqUGDBvL19VXHjh116NChq+43LS1N3bt3l7+/vwICAvTnP/9ZGRkZkqQFCxaoSZMmkqRbb71VNputyG0GBgYWqv3NN9/Uhg0btHz5clWvXl1S4VPAHTp00KBBgzRo0CAFBgaqevXqGjNmjIwxl613//796t69u4KCguTv76/WrVvriy++cOoTHh6uSZMmqW/fvgoICHActfzpp5/Uq1cvVatWTZUqVVKrVq20adMmp3XffvtthYeHKzAwUI8++qhyc3Od6r10Cnj06NFq27ZtofoiIyM1ceJEx/y8efMUEREhHx8fNWrUSP/6178uOzZcGQEQAGBJEyZM0J///Gd999136tKli2JjY3Xq1ClJ0pEjR/TQQw+pW7duSk1N1ZNPPqmRI0decXt2u13du3fXqVOn9NVXX2nNmjU6cOCAevbsKUnq2bOnI1ylpKTo+PHjCgsLu2qdK1as0NixY5WYmKjIyMgr9l24cKEqVKiglJQUzZw5UzNmzNC8efMu2z8vL09dunRRcnKytm3bpk6dOqlbt25KS0tz6jd9+nRFRkZq27ZtGjNmjPLy8tS+fXsdPXpUH3/8sbZv364XXnhBdrvdsc7+/fu1fPlyrVixQitWrNBXX31V5JFNSYqNjVVKSor279/vaPvhhx/03XffqXfv3pKkpKQkjR07VpMnT9auXbs0ZcoUjRkzxnFKHNfIuJFZs2aZ2rVrG29vb9OmTRuzadOmK/ZfunSpadiwofH29ja33367Wbly5TXtLzs720gqnSleRuP/78/S2kdZ7udGq4Ux8lpYfGrYsKHZvHmzOX36tPMHX8uWxoSGlv3UsmWxP5v79etnPD09TaVKlZymyZMnO/pIMi+99JJjPi8vz0gyn376qTHGmFGjRpnGjRs7bXfEiBFGkvnll1+K3O/nn39uPD09TVpamqPthx9+MJJMSkqKMcaYbdu2GUnm4MGDxRrLrl27TEBAgHnxxReLHGf37t0d8+3btzcRERHGbrc71RwREVGsfV1y2223mddee80xX7t2bdOjRw+nPv/+979N5cqVzcmTJ4vcxrhx44yfn5/JyclxtA0fPty0bdvWqd7Bgwc75iMjI83EiRMd86NGjXLqX7duXbNo0SKn/UyaNMlERUUVWcPp06fN5s2bzf33329atmxpqlevXujnPDs7+0ovhVtzm2sAlyxZovj4eM2ZM0dt27ZVQkKCYmJitHv3btWoUaNQ//Xr16tXr16aOnWq/vjHP2rRokXq0aOHtm7dqttvv90FIwCAG0B6unT0qKuruKqOHTvq9ddfd2r77TVnTZs2dfy9UqVKCggI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pXr16qlly5aaOnWqIiMjNXPmTLcb55YtW5SZmakWLVqoQoUKqlChgr766iu9+uqrqlChgoKCgtxqvP+rSpUqatCggfbt2+d272tISIgaN27s1BYREeE45e2On02HDx/WF198oSeffNLR5m7v6/DhwzVy5Eg9+uijatKkifr06aOhQ4dq6tSpktzzfS0JBMByxMvLSy1btlRycrKjzW63Kzk5WVFRUS6srGTVqVNHwcHBTuPMycnRpk2bHOOMiopSVlaWtmzZ4ujz5Zdfym63q23btmVe8+UYYzRo0CAtW7ZMX375perUqeO0vGXLlqpYsaLTWHfv3q20tDSnsX7//fdOHz5r1qxRQEBAoV9W5Y3dbtf58+fdbpz33nuvvv/+e6WmpjqmVq1aKTY21vF3dxrv/8rLy9P+/fsVEhLidu/rnXfeWegxTXv27FHt2rUluddn0yWJiYmqUaOGunbt6mhzt/f1zJkz8vBwjjOenp6y2+2S3PN9LRGuvgsFzhYvXmy8vb3NggULzM6dO81TTz1lqlSp4nQn1o0gNzfXbNu2zWzbts1IMjNmzDDbtm0zhw8fNsb8ekt+lSpVzEcffWS+++4707179yJvyW/evLnZtGmT+frrr039+vXL3S35zz77rAkMDDTr1q1zeuTCmTNnHH2eeeYZU6tWLfPll1+azZs3m6ioKBMVFeVYfulxC/fff79JTU01q1evNjfffHO5e9zCyJEjzVdffWUOHjxovvvuOzNy5Ehjs9nM559/boxxn3Fezv/eBWyM+4z3r3/9q1m3bp05ePCg+eabb0x0dLSpXr26yczMNMa4zziN+fWRPhUqVDCTJ082e/fuNUlJScbPz8+88847jj7u8tlkzK9PkahVq5YZMWJEoWXu9L7269fPhIaGOh4D8+GHH5rq1aubF154wdHHnd7XkkIALIdee+01U6tWLePl5WXatGljNm7c6OqSrtnatWuNpEJTv379jDG/3pY/ZswYExQUZLy9vc29995rdu/e7bSNkydPml69ehl/f38TEBBg+vfvb3Jzc10wmssraoySTGJioqPP2bNnzV/+8hdTtWpV4+fnZx588EFz/Phxp+0cOnTIdO7c2fj6+prq1aubv/71r+bChQtlPJore/zxx03t2rWNl5eXufnmm829997rCH/GuM84L+e3AdBdxtuzZ08TEhJivLy8TGhoqOnZs6fTc/HcZZyXfPLJJ+b222833t7eplGjRuaNN95wWu4un03GGPPZZ58ZSYXqN8a93tecnBwzePBgU6tWLePj42NuvfVW8+KLLzo9rsad3teSYjPmfx6VDQAAALfHNYAAAAAWQwAEAACwGAIgAACAxRAAAQAALIYACAAAYDEEQAAAAIshAAIAAFgMARDA/2vvbkKi+uIwjn8HZERtxjfMoUEGR02uugnaKJoYDY7SRoxaFAotXIkRFNhCSXxDN25CplZKiQjmSgWlYnypFkmLUAYdFcmFs3EyEMPA6b8bmL8ujJRi7vOBu/mdew/nt3s43HOvKR0cHFBfX4/dbsdisbC3t3diTUQkHikAikjc2d7e5v79+1y6dAmr1YrL5eLBgwfs7u5G7xkeHmZhYYEPHz6ws7NDamrqibU/4ff7FSRF5J+kACgicWVzc5OrV/kFy8kAAAOQSURBVK8SDAYZHR1lfX0dn8/H27dvKS0tJRwOA7CxsYFhGJSUlOBwOLBYLCfWRETikX4FJyJxpaamhuXlZdbW1khKSorWQ6EQeXl5NDQ0EAgEmJubi45VVlYCHKv5/X4GBwcZGBhge3ub1NRUKioqGB8fByASidDX18eLFy8IhUJcvnyZtrY2bt26xdbWFrm5uTFra2xsZGho6By7FxE5nYS/vQARkbMSDoeZmZmhu7s7JvwBOBwO7t69y9jYGMFgkCdPnrC8vMzExARWqxWA1tbWmNrS0hItLS28fPmSsrIywuEwCwsL0Tl7e3t59eoVPp+PgoIC5ufnuXfvHllZWZSXl/P69Wvq6+tZXV3FbrcfW5OIyN+iACgicSMYDPLr1y8Mwzhx3DAMvn37xtHREcnJyVitVhwOR3T8/zW/309KSgo3b97EZrPhcrm4cuUKAIeHh/T09PDmzRtKS0sBcLvdLC4u8vz5cyorK8nIyADg4sWLpKWlnWfrIiK/RQFQROLOWb3Z4vF4cLlcuN1uvF4vXq+Xuro6kpOTWV9f5+DgAI/HE/PMz58/oyFRRORfpQAoInEjPz8fi8VCIBCgrq7u2HggECA9PZ2srKxTzWez2fj8+TN+v5/Z2Vna29t5+vQpnz59Yn9/H4CpqSmcTmfMc4mJiX/ejIjIOdIpYBGJG5mZmXg8HgYHB/nx40fMWCgUYmRkhDt37vzW6d6EhARu3LhBf38/X758YWtri3fv3lFUVERiYiJfv34lPz8/5srJyQGIvlt4dHR0dk2KiJwB7QCKSFx59uwZZWVlVFdX09XVRW5uLisrKzx+/Bin00l3d/ep55qcnGRzc5Nr166Rnp7O9PQ0kUiEwsJCbDYbjx494uHDh0QiEcrLy/n+/Tvv37/HbrfT2NiIy+XCYrEwOTlJbW0tSUlJXLhw4Ry7FxE5He0AikhcKSgoYGlpCbfbze3bt8nLy6OpqYmqqio+fvwYPZhxGmlpaUxMTHD9+nUMw8Dn8zE6OkpxcTEAnZ2dtLW10dvbi2EYeL1epqamop9/cTqddHR00NraSnZ2Ns3NzefSs4jI79J3AEVERERMRjuAIiIiIiajACgiIiJiMgqAIiIiIiajACgiIiJiMgqAIiIiIiajACgiIiJiMgqAIiIiIiajACgiIiJiMgqAIiIiIiajACgiIiJiMgqAIiIiIiajACgiIiJiMv8B1GY63YW86TwAAAAASUVORK5CYII=",
"plugin_version": "0.5.2",
"signature_analysis": "\nDECIMAL HEXADECIMAL DESCRIPTION\n--------------------------------------------------------------------------------\n0 0x0 Zip archive data, at least v2.0 to extract, name: get_files_test/\n45 0x2D Zip archive data, at least v2.0 to extract, name: get_files_test/generic folder/\n105 0x69 Zip archive data, at least v1.0 to extract, compressed size: 20, uncompressed size: 20, name: get_files_test/generic folder/test file 3_.txt\n201 0xC9 Zip archive data, at least v2.0 to extract, compressed size: 59, uncompressed size: 62, name: get_files_test/testfile1\n314 0x13A Zip archive data, at least v1.0 to extract, compressed size: 28, uncompressed size: 28, name: get_files_test/testfile2\n765 0x2FD End of Zip archive, footer length: 22\n\n",
"summary": {
"End of Zip archive": [
"418a54d78550e8584291c96e5d6168133621f352bfc1d43cf84e81187fef4962_787"
],
"Zip archive data": [
"418a54d78550e8584291c96e5d6168133621f352bfc1d43cf84e81187fef4962_787"
]
}
},
"file_hashes": {
"analysis_date": 1548333208.4131176,
"imphash": None,
"md5": "743692a4121ff9f0c492c14a8371a32e",
"plugin_version": "1.0",
"ripemd160": "6cb1094fd083fe21c5ebba5426e3863f77f85d11",
"sha1": "105bc9f473fa46553bc256521b9b0c5e29213d69",
"sha256": "418a54d78550e8584291c96e5d6168133621f352bfc1d43cf84e81187fef4962",
"sha512": "bf9fa25242fecaa8e2b58d01758e5d7d9779487594cbb43ec9665d1df5ae967faabc625764715296bb663a88e6c01cca3b862c33e9d093df79e595b47fa68255",
"ssdeep": "12:5DJhWmNJAx9DV1JzAkVTDL4EZFJhudt6JA1uL33k9S/OgRI:ThWm7Ax9DVLAe4EZhueAk3k9SWf",
"summary": None,
"whirlpool": "fdb19c4ed557ce8c1e5d7972008c9e83a5c82501a1057f9dbae083762a653b264e0ddeec25a6933f00fe7273e80bf8066904425119a544ea2161ef8ec9c3ecc0"
},
"file_type": {
"analysis_date": 1548333203.6747785,
"full": "Zip archive data, at least v2.0 to extract",
"mime": "application/zip",
"plugin_version": "1.0",
"summary": {
"application/zip": [
"418a54d78550e8584291c96e5d6168133621f352bfc1d43cf84e81187fef4962_787"
],
"text/plain": [
"d558c9339cb967341d701e3184f863d3928973fccdc1d96042583730b5c7b76a_62",
"faa11db49f32a90b51dfc3f0254f9fd7a7b46d0b570abd47e1943b86d554447a_28",
"289b5a050a83837f192d7129e4c4e02570b94b4924e50159fad5ed1067cfbfeb_20"
]
}
},
"known_vulnerabilities": {
"analysis_date": 1548333209.475375,
"plugin_version": "0.2",
"summary": {},
"system_version": "3.7.1_1548244221"
},
"malware_scanner": {
"analysis_date": 1548333207.3892179,
"md5": "743692a4121ff9f0c492c14a8371a32e",
"number_of_scanners": 1,
"plugin_version": "0.3.1",
"positives": 0,
"scanners": [
"ClamAV"
],
"scans": {
"ClamAV": {
"detected": False,
"result": "clean",
"version": "ClamAV 0.100.2/25326/Thu Jan 24 03:30:43 2019\n"
}
},
"summary": {},
"system_version": "0.2.6"
},
"printable_strings": {
"analysis_date": 1548333208.388212,
"plugin_version": "0.3.4",
"skipped": "blacklisted file type",
"summary": {}
},
"software_components": {
"analysis_date": 1548333204.2639465,
"plugin_version": "0.3.2",
"summary": {
"Test Software 1.2.3": [
"d558c9339cb967341d701e3184f863d3928973fccdc1d96042583730b5c7b76a_62"
]
},
"system_version": "3.7.1_1548244221"
},
"unpacker": {
"analysis_date": 1548333203.557019,
"entropy": 0.5789618884873324,
"number_of_unpacked_files": 3,
"output": "\n7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21\np7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,8 CPUs x64)\n\nScanning the drive for archives:\n1 file, 787 bytes (1 KiB)\n\nExtracting archive: /media/data/fact_fw_data/41/418a54d78550e8584291c96e5d6168133621f352bfc1d43cf84e81187fef4962_787\n--\nPath = /media/data/fact_fw_data/41/418a54d78550e8584291c96e5d6168133621f352bfc1d43cf84e81187fef4962_787\nType = zip\nPhysical Size = 787\n\nEverything is Ok\n\nFolders: 2\nFiles: 3\nSize: 110\nCompressed: 787\n",
"plugin_used": "7z",
"plugin_version": "0.7",
"size packed -> unpacked": "459.00 Byte -> 110.00 Byte",
"summary": {
"data lost": [
"418a54d78550e8584291c96e5d6168133621f352bfc1d43cf84e81187fef4962_787"
],
"unpacked": [
"d558c9339cb967341d701e3184f863d3928973fccdc1d96042583730b5c7b76a_62",
"faa11db49f32a90b51dfc3f0254f9fd7a7b46d0b570abd47e1943b86d554447a_28",
"289b5a050a83837f192d7129e4c4e02570b94b4924e50159fad5ed1067cfbfeb_20"
]
}
},
"users_and_passwords": {
"analysis_date": 1548333206.30962,
"plugin_version": "0.4.1",
"summary": {}
}
},
"meta_data": {
"device_class": "Test-Data",
"device_name": "test_container",
"device_part": "",
"hid": "Frauhhofer FKIE test_container v. 0.1",
"number_of_files": 3,
"release_date": "2019-01-24",
"size": 787,
"vendor": "Frauhhofer FKIE",
"version": "0.1"
}
},
"request": {
"uid": "418a54d78550e8584291c96e5d6168133621f352bfc1d43cf84e81187fef4962_787"
},
"request_resource": "/rest/firmware",
"status": 0,
"timestamp": 1548333492
}
import pytest
from pdf_generator.tex_generation.template_engine import TemplateEngine
from test.data.test_dict import TEST_DICT
# pylint: disable=redefined-outer-name
TEST_DATA = {
'analysis': {'file_hashes': {'ssdeep': 'bla', 'sha1': 'blah'}},
'meta_data': {'device_name': 'test_device'}
}
@pytest.fixture(scope='function')
def stub_engine():
return TemplateEngine()
def test_latex_code_generation(stub_engine: TemplateEngine):
result = stub_engine.render_meta_template(TEST_DICT)
assert result
def test_render_template(tmpdir):
engine = TemplateEngine(template_folder='test', tmp_dir=tmpdir)
test_data = {'meta_data': '123', 'analysis': '456'}
output = engine.render_analysis_template(plugin='render_test', analysis=test_data)
assert output == 'Test - '
import json
from pathlib import Path
import pytest
from pdf_generator.generator import (
LOGO_FILE, MAIN_TEMPLATE, META_TEMPLATE, PLUGIN_TEMPLATE_BLUEPRINT, copy_fact_image, create_report_filename,
create_templates, execute_latex, render_analysis_templates
)
class MockEngine:
def __init__(self, *_, **__):
pass
@staticmethod
def render_main_template(analysis, meta_data):
return '{}\n{}'.format(json.dumps(analysis), json.dumps(meta_data))
@staticmethod
def render_meta_template(meta_data):
return json.dumps(meta_data)
@staticmethod
def render_analysis_template(_, analysis):
return json.dumps(analysis)
def exec_mock(*_, **__):
Path('test').write_text('works')
def test_execute_latex(monkeypatch, tmpdir):
monkeypatch.setattr('pdf_generator.generator.execute_shell_command', exec_mock)
execute_latex(str(tmpdir))
assert Path(str(tmpdir), 'test').exists()
assert Path(str(tmpdir), 'test').read_text() == 'works'
def test_copy_fact_image(tmpdir):
copy_fact_image(str(tmpdir))
assert Path(str(tmpdir), LOGO_FILE).exists()
@pytest.mark.parametrize('device_name, pdf_name', [
('simple', 'simple_analysis_report.pdf'),
('harder name', 'harder_name_analysis_report.pdf'),
('dangerous/name', 'dangerous__name_analysis_report.pdf')
])
def test_create_report_filename(device_name, pdf_name):
assert create_report_filename({'device_name': device_name}) == pdf_name
def test_create_analysis_templates():
templates = render_analysis_templates(engine=MockEngine(), analysis={'test': {'result': 'data'}})
assert len(templates) == 1
filename, result_code = templates[0]
assert filename == PLUGIN_TEMPLATE_BLUEPRINT.format('test')
assert result_code == '{"result": "data"}'
def test_create_templates(monkeypatch, tmpdir):
monkeypatch.setattr('pdf_generator.generator.TemplateEngine', MockEngine)
create_templates(analysis={'test': {'result': 'data'}}, meta_data={}, tmp_dir=str(tmpdir))
assert Path(str(tmpdir), MAIN_TEMPLATE).exists()
assert Path(str(tmpdir), META_TEMPLATE).exists()
assert Path(str(tmpdir), PLUGIN_TEMPLATE_BLUEPRINT.format('test')).exists()
assert Path(str(tmpdir), PLUGIN_TEMPLATE_BLUEPRINT.format('test')).read_text() == '{"result": "data"}'
from pathlib import Path
import pytest
from pdf_generator.tex_generation.template_engine import (
TemplateEngine, decode_base64_to_file, render_number_as_size, render_number_as_string, render_unix_time,
replace_characters_in_list, replace_special_characters, split_hash_string, split_long_lines
)
# pylint: disable=redefined-outer-name
@pytest.fixture(scope='function')
def stub_engine(tmpdir):
return TemplateEngine(template_folder='test', tmp_dir=tmpdir)
def test_byte_number_filter():
assert render_number_as_size(None) == 'not available'
assert render_number_as_size(12, verbose=False) == '12.00 Byte'
assert render_number_as_size(128000) == '125.00 KiB (128,000 Byte)'
assert render_number_as_size(128000, verbose=False) == '125.00 KiB'
def test_nice_number_filter():
assert render_number_as_string(None) == 'not available'
assert render_number_as_string('no int') == 'not available'
assert render_number_as_string(12) == '12'
assert render_number_as_string(12.1) == '12.10'
assert render_number_as_string(12.101) == '12.10'
assert render_number_as_string(12.109) == '12.11'
assert render_number_as_string('12') == '12'
@pytest.mark.skip(reason='Since local time used, result is not stable')
def test_nice_unix_time():
assert render_unix_time(None) == 'not available'
assert render_unix_time(10) == '1970-01-01 01:00:10'
def test_split_hash():
assert split_hash_string('X' * 62) == '{}\nX'.format('X' * 61)
assert split_hash_string('X' * 61) == 'X' * 61
def test_split_output_lines():
assert split_long_lines('X\nX') == 'X\nX'
assert split_long_lines('{}\nX'.format('X' * 93)) == '{}\nX\nX'.format('X' * 92)
def test_convert_base64_to_png_filter(tmpdir):
decode_base64_to_file('0000', 'testfile', str(tmpdir))
assert Path(str(tmpdir), 'testfile.png').read_bytes() == b'\xd3\x4d\x34'
def test_filter_latex_special_chars():
assert replace_special_characters('safe') == 'safe'
assert replace_special_characters(r'C:\Windows') == r'C:Windows'
assert replace_special_characters(r'100 $') == r'100 \$'
def test_filter_chars_in_list():
assert replace_characters_in_list([]) == []
assert replace_characters_in_list([r'safe', r'un\safe']) == ['safe', 'unsafe']
def test_render_meta_template(stub_engine):
assert stub_engine.render_meta_template(meta_data='anything') == 'Test anything - '
def test_render_main_template(stub_engine):
assert stub_engine.render_main_template(meta_data='anything', analysis='else') == 'Test anything - else'
def test_render_analysis_template(stub_engine):
assert stub_engine.render_analysis_template(plugin='non_existing', analysis='result') == 'Presenting: result'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment