Skip to content

Commit

Permalink
Add new HTML report format
Browse files Browse the repository at this point in the history
This commit adds a new report plugin that formats the output report
to render as an HTML file that can be viewed in a web browser. To
generate the html report, a report dictionary is created, similar
to how the json report is formatted. The dictionary is then
translated to html code. If a user does not provide an output file
when they use the html formatting option, the report will be created
in a default report.html file.

Resolves #614

Signed-off-by: abhaykatheria <abhay.katheria1998@gmail.com>
  • Loading branch information
abhaykatheria committed Jun 23, 2020
1 parent 352e037 commit aca49c6
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 1 deletion.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ tern.formats =
spdxtagvalue = tern.formats.spdx.spdxtagvalue.generator:SpdxTagValue
json = tern.formats.json.generator:JSON
yaml = tern.formats.yaml.generator:YAML
html = tern.formats.html.generator:HTML
tern.extensions =
cve_bin_tool = tern.extensions.cve_bin_tool.executor:CveBinTool
scancode = tern.extensions.scancode.executor:Scancode
Expand Down
2 changes: 1 addition & 1 deletion tern/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def main():
metavar='REPORT_FORMAT',
help="Format the report using one of the "
"available formats: "
"spdxtagvalue, json, yaml")
"spdxtagvalue, json, yaml, html")
parser_report.add_argument('-o', '--output-file', default=None,
metavar='FILE',
help="Write the report to a file. "
Expand Down
4 changes: 4 additions & 0 deletions tern/formats/html/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2020 VMware, Inc. All Rights Reserved.
# SPDX-License-Identifier: BSD-2-Clause
300 changes: 300 additions & 0 deletions tern/formats/html/generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2020 VMware, Inc. All Rights Reserved.
# SPDX-License-Identifier: BSD-2-Clause

"""
HTML document generator
"""

import logging

from tern.formats import generator
from tern.report.content import get_tool_version, get_licenses_only
from tern.utils import constants


# global logger
logger = logging.getLogger(constants.logger_name)


css = '''
<style>
ul, #myUL {
list-style-type: none;
}
li {
padding-top: 5px;padding-bottom: 5px;
}
#myUL {
margin: 0;
padding: 0;
}
.caret {
cursor: pointer;
user-select: none;
font-family: 'Inconsolata', monospace;
}
.caret::before {
content: " \\25B6";
color: ;
display: inline-block;
margin-right: 6px;
}
.caret-down::before {
transform: rotate(90deg);
}
.nested {
display: none;
}
.active {
display: block;
}
.header {
text-align: left;
background: #f1f1f1;
}
.text-c {
color:green;
font-family:'Inconsolata',monospace;
}
.text-h {
font-family:'Inconsolata',monospace;
}
</style>\n
'''


js = '''
<script>
var toggler = document.getElementsByClassName("caret");
var i;
for (i = 0; i < toggler.length; i++) {
toggler[i].addEventListener("click", function() {
this.parentElement.querySelector(".nested").classList.toggle("active");
this.classList.toggle("caret-down");
});
}
</script>\n
'''


head = """
<!DOCTYPE html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@300&family=Oswald&display=swap"
rel="stylesheet"><link href="https://fonts.googleapis.com/css2?family=Josefin+Sans:ital,wght@1,300&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inconsolata&display=swap" rel="stylesheet">
%s \n
</head>
<body>
<div class="header">
<img src="https://raw.githubusercontent.com/tern-tools/tern/master/docs/img/tern_logo.png" height="60px">\n
<br>
</div>\n
<div style="font-family: \'Inconsolata\', monospace;">
<p>
Tern at %s
<p>
The following report was generated for "%s" image.
</div>
"""


def image_handler(list_obj, indent):
'''Write html code for the images list in the report with
image name as title'''
html_string = ''
for i in list_obj:
html_string = html_string + dict_handler(i, indent+1)
return html_string


def manifest_handler(list_obj, indent):
'''Write html code for the manifests list in the report
with config as title'''
html_string = ' '*indent + '<ul class ="nested"> \n'
for l in list_obj:
html_string = html_string + ' '*indent + '<li><span class="caret">' \
+ str(l["Config"][:10]) + ' : ' + '</span> \n '
html_string = html_string + dict_handler(l, indent+1)
html_string = html_string + ' '*indent + '</li> \n'
html_string = html_string + ' '*indent + '</ul> \n'
return html_string


def layers_handler(list_obj, indent):
'''Write html code for the origins list in the report
with tar_file hash as title'''
html_string = ' '*indent + '<ul class ="nested"> \n'
for l in list_obj:
html_string = html_string + ' '*indent + '<li><span class="caret">' \
+ str(l["tar_file"][:10]) + ' : ' + '</span> \n '
html_string = html_string + dict_handler(l, indent+1)
html_string = html_string + ' '*indent + '</li> \n'
html_string = html_string + ' '*indent + '</ul> \n'
return html_string


def history_handler(list_obj, indent):
'''Write html code for the history list in the report
with time-stamp as title'''
html_string = ' '*indent + '<ul class ="nested"> \n'
for l in list_obj:
html_string = html_string + ' '*indent + '<li><span class="caret">' \
+ str(l["created"][:19]) + ' : ' + '</span> \n '
html_string = html_string + dict_handler(l, indent+1)
html_string = html_string + ' '*indent + '</li> \n'
html_string = html_string + ' '*indent + '</ul> \n'
return html_string


def origins_handler(list_obj, indent):
'''Write html code for the origins list in the report
with origin string as title'''
html_string = ' '*indent + '<ul class ="nested"> \n'
for l in list_obj:
html_string = html_string + ' '*indent + '<li><span class="caret">' \
+ str(l["origin_str"]) + '</span> \n '
html_string = html_string + dict_handler(l, indent+1)
html_string = html_string + ' '*indent + '</li> \n'
html_string = html_string + ' '*indent + '</ul> \n'
return html_string


def list_handler(list_obj, indent):
'''Write html code for lists in report dictionary'''
html_string = ''
for i, _ in enumerate(list_obj):
if isinstance(list_obj[i], dict):
if "name" in list_obj[i].keys():
html_string = html_string + ' '*indent + \
'<li><span class="caret">' + str(list_obj[i]["name"]) + \
' : ' + '</span> \n '
else:
html_string = html_string + ' '*indent + \
'<li><span class="caret">' + str(i) + ' : ' + '</span> \n '
html_string = html_string + dict_handler(list_obj[i], indent+1)
html_string = html_string + ' '*indent + '</li> \n '
elif isinstance(list_obj[i], list):
html_string = html_string + ' '*indent + \
'<li><span class="caret">' + str(i) + ' : ' + '</span> \n '
html_string = html_string + ' '*indent + \
'<ul class ="nested"> \n '
html_string = html_string + list_handler(list_obj[i], indent+1)
html_string = html_string + ' '*indent + '</ul> \n ' + \
' '*indent + '</li>\n '
else:
html_string = html_string + ' '*indent + '<li>' + \
'<span class="text-c">' + str(list_obj[i]) + \
'</span>\n</li> \n '
return html_string


# pylint: disable=too-many-branches
def dict_handler(dict_obj, indent):
'''Writes html code for dictionary in report dictionary'''
html_string = ''
html_string = html_string + ' '*indent + '<ul class ="nested"> \n'
for k, v in dict_obj.items():
if isinstance(v, dict):
if "name" in v.keys():
html_string = html_string + ' '*indent + \
'<li><span class="caret">' + str(v["name"]) + ' : ' + \
'</span> \n '
else:
html_string = html_string + ' '*indent + \
'<li><span class="caret">' + str(k) + ' : ' + '</span> \n '
html_string = html_string + dict_handler(v, indent+1) + \
' '*indent + '</li> \n '
elif isinstance(v, list):
html_string = html_string + ' '*indent + \
'<li><span class="caret">' + str(k) + ' : ' + \
'[%d]' % (len(v)) + '</span> \n '
if k == "images":
html_string = html_string + image_handler(v, indent) + \
' '*indent + '</li>\n'
elif k == "manifest":
html_string = html_string + manifest_handler(v, indent) + \
' '*indent + '</li>\n'
elif k == "layers":
html_string = html_string + layers_handler(v, indent) + \
' '*indent + '</li>\n'
elif k == "origins":
html_string = html_string + origins_handler(v, indent) + \
' '*indent + '</li>\n'
elif k == "history":
html_string = html_string + history_handler(v, indent) + \
' '*indent + '</li>\n'
else:
html_string = html_string + ' '*indent + \
'<ul class ="nested"> \n ' + list_handler(v, indent+1) + \
' '*indent + '</ul> \n ' + ' '*indent + '</li> \n '
else:
html_string = html_string + ' '*indent + \
'<li><span class="text-h">' + str(k) + ' : ' + \
'</span><span class="text-c">' + str(v) + '</span></li>\n'
html_string = html_string + ' '*indent + '</ul> \n '
return html_string


def report_dict_to_html(dict_obj):
'''Writes html code for report'''
html_string = ''
html_string = html_string + '<ul class ="myUL"> \n'
html_string = html_string + \
'<li><span class="caret">REPORT DETAILS</span> \n'
html_string = html_string + dict_handler(dict_obj, 0)
html_string = html_string + '</li></ul> \n'
return html_string


def write_licenses(image_obj_list):
'''Adds licenses to top of the page'''
licenses = get_licenses_only(image_obj_list)
html_string = ''
html_string = html_string + '<ul class ="myUL"> \n'
html_string = html_string + '<li><span class="caret">Summary of \
Licenses Found</span> \n'
html_string = html_string + '<ul class ="nested"> \n'
for l in licenses:
html_string = html_string + \
'<li style="font-family: \'Inconsolata\' , monospace;" >' + \
l + '</li>\n'
html_string = html_string + '</ul></li></ul> \n'
return html_string


def create_html_report(report_dict, image_obj_list):
'''Return the html report as a string'''
logger.debug("Creating HTML report...")
report = ''
report = report + '\n' + head % (css, get_tool_version(),
report_dict['images'][0]['image']['name']
+ ':' +
report_dict['images'][0]['image']['tag'])
report = report + '\n' + write_licenses(image_obj_list)
report = report + '\n' + report_dict_to_html(report_dict)
report = report + '\n' + js
report = report + '\n' + '</body>\n</html>\n'
return report


def get_report_dict(image_obj_list):
'''Given an image object list, return a python dict of the report'''
image_list = []
for image in image_obj_list:
image_list.append({'image': image.to_dict()})
image_dict = {'images': image_list}
return image_dict


class HTML(generator.Generate):
def generate(self, image_obj_list):
'''Given a list of image objects, create a html report
for the images'''
report_dict = get_report_dict(image_obj_list)
report = create_html_report(report_dict, image_obj_list)
return report
2 changes: 2 additions & 0 deletions tern/report/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def write_report(report, args):
'''Write the report to a file'''
if args.output_file:
file_name = args.output_file
elif args.report_format == 'html':
file_name = constants.html_file
else:
file_name = constants.report_file
with open(file_name, 'w') as f:
Expand Down
2 changes: 2 additions & 0 deletions tern/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,7 @@
mergedir = 'mergedir'
# report file
report_file = 'report.txt'
# html report file
html_file = 'report.html'
# locked dockerfile
locked_dockerfile = 'Dockerfile.lock'

0 comments on commit aca49c6

Please sign in to comment.