mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
feat: add html template (#1806)
- responsive template using datatables.js - filtering option - pdf export option Signed-off-by: Firas AlShafei <firas.alshafei@hitachienergy.com>
This commit is contained in:
parent
6dde5ce9f4
commit
8e1cce03c8
2 changed files with 617 additions and 0 deletions
|
@ -9,6 +9,9 @@ Current templates:
|
|||
<pre>
|
||||
.
|
||||
├── README.md
|
||||
├── html.tmpl
|
||||
├── junit.tmpl
|
||||
├── csv.tmpl
|
||||
└── table.tmpl
|
||||
</pre>
|
||||
|
||||
|
@ -21,3 +24,22 @@ This template mimics the "default" table output of Grype, there are some drawbac
|
|||
- no (wont-fix) logic
|
||||
|
||||
As you can see from the above list, it's not perfect but it's a start.
|
||||
|
||||
## HTML
|
||||
|
||||
Produces a nice html template with a dynamic table using datatables.js.
|
||||
|
||||
You can also modify the templating filter to limit the output to a subset.
|
||||
|
||||
Default includes all
|
||||
|
||||
```
|
||||
{{- if or (eq $vuln.Vulnerability.Severity "Critical") (eq $vuln.Vulnerability.Severity "High") (eq $vuln.Vulnerability.Severity "Medium") (eq $vuln.Vulnerability.Severity "Low") (eq $vuln.Vulnerability.Severity "Unknown") }}
|
||||
```
|
||||
|
||||
We can limit it to only Critical, High, and Medium by editing the filter as follows
|
||||
|
||||
```
|
||||
{{- if or (eq $vuln.Vulnerability.Severity "Critical") (eq $vuln.Vulnerability.Severity "High") (eq $vuln.Vulnerability.Severity "Medium") }}
|
||||
```
|
||||
|
||||
|
|
595
templates/html.tmpl
Normal file
595
templates/html.tmpl
Normal file
|
@ -0,0 +1,595 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Vulnerability Report</title>
|
||||
|
||||
<!-- Template Metadata-->
|
||||
<meta name="author" content="grype">
|
||||
<meta name="version" content="1.0.0">
|
||||
|
||||
<!-- Source datatables.js and its dependencies -->
|
||||
<link
|
||||
href="https://cdn.datatables.net/v/dt/jq-3.7.0/jszip-3.10.1/dt-2.0.3/b-3.0.1/b-html5-3.0.1/b-print-3.0.1/r-3.0.1/datatables.min.css"
|
||||
rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
|
||||
<script
|
||||
src="https://cdn.datatables.net/v/dt/jq-3.7.0/jszip-3.10.1/dt-2.0.3/b-3.0.1/b-html5-3.0.1/b-print-3.0.1/r-3.0.1/datatables.min.js"></script>
|
||||
|
||||
<!-- Page & Main Container -->
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
height: 100vh;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
<!-- Header Container -->
|
||||
<style>
|
||||
.heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* Aligns children to the far left and far right */
|
||||
align-items: center;
|
||||
/* Centers children vertically within the container */
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 5px;
|
||||
background-color: #e8f0fe;
|
||||
/* Muted blue background */
|
||||
color: #495057;
|
||||
/* Dark gray text color */
|
||||
}
|
||||
|
||||
.heading-left,
|
||||
.heading-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Organizes children vertically */
|
||||
align-items: flex-start;
|
||||
/* Aligns text and other content to the start */
|
||||
}
|
||||
|
||||
.heading-right {
|
||||
align-items: flex-end;
|
||||
/* Aligns the image to the far right */
|
||||
padding-right: 20px;
|
||||
/* Adds padding to the right of the .heading-right container */
|
||||
}
|
||||
|
||||
.heading-right img {
|
||||
width: 70px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.heading-left h1,
|
||||
.heading-left p {
|
||||
padding-left: 20px;
|
||||
margin: 4px 0;
|
||||
/* Reduces the vertical margin to tighten the spacing */
|
||||
}
|
||||
</style>
|
||||
<!-- Severity Information Container -->
|
||||
<style>
|
||||
.severity-info {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
/* Evenly spaces the severity boxes */
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.severity-box {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Stack children vertically */
|
||||
justify-content: space-between;
|
||||
/* Space between the title and count */
|
||||
align-items: center;
|
||||
/* Center align items horizontally */
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
margin: 5px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
/* Necessary for absolute positioning of children */
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
/* Smooth transitions for interactive effects */
|
||||
}
|
||||
|
||||
.severity-title {
|
||||
font-size: 0.8em;
|
||||
position: absolute;
|
||||
/* Positioning it absolutely to stick to the bottom */
|
||||
bottom: 10px;
|
||||
/* Distance from the bottom of the container */
|
||||
width: 100%;
|
||||
/* Ensure it spans the width of the box */
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
/* Light color for visibility */
|
||||
}
|
||||
|
||||
.severity-count {
|
||||
font-size: 3em;
|
||||
/* Increasing font size for better visibility and impact */
|
||||
margin-bottom: 20px;
|
||||
/* Ensures space between the count and the bottom-positioned title */
|
||||
color: #fff;
|
||||
/* Ensuring text is always white for visibility */
|
||||
}
|
||||
|
||||
.severity-box.active {
|
||||
border: 2px solid #007bff;
|
||||
/* Highlight active box */
|
||||
}
|
||||
|
||||
/* Example ID-based styles, ensure to add similar styles for each severity level */
|
||||
#critical {
|
||||
background-color: #d9534f;
|
||||
}
|
||||
|
||||
#high {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
||||
#medium {
|
||||
background-color: #5bc0de;
|
||||
}
|
||||
|
||||
#low {
|
||||
background-color: #5cb85c;
|
||||
}
|
||||
|
||||
#unknown {
|
||||
background-color: #777;
|
||||
}
|
||||
|
||||
.severity-box:hover {
|
||||
transform: translateY(-5px);
|
||||
/* Creates a subtle "pop" effect */
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
/* Adds depth on hover */
|
||||
}
|
||||
</style>
|
||||
<!-- Table Control & Datatables Wrapper -->
|
||||
<style>
|
||||
.dataTables_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* Ensures maximum space between items */
|
||||
align-items: center;
|
||||
/* Vertically centers items in the container */
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
margin-top: 10px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Apply order properties to reposition elements visually */
|
||||
.dt-buttons {
|
||||
order: 2;
|
||||
/* Places buttons on the right */
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
/* Aligns buttons to the right */
|
||||
}
|
||||
|
||||
.dataTables_filter {
|
||||
order: 1;
|
||||
/* Places search on the left */
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dataTables_filter label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dataTables_filter input {
|
||||
width: 100%;
|
||||
/* Allows the input to take full width of its container */
|
||||
padding: 8px;
|
||||
margin-left: 5px;
|
||||
/* Provides a little space after the label */
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
background-color: #007bff;
|
||||
/* Primary blue */
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
.dataTables_wrapper .dt-buttons,
|
||||
.dataTables_wrapper .dataTables_filter {
|
||||
float: none;
|
||||
text-align: center;
|
||||
margin: 10px auto;
|
||||
/* Centers and adds margin */
|
||||
display: block;
|
||||
/* each takes full width */
|
||||
width: 90%;
|
||||
/* less than full width for padding */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- Table Container -->
|
||||
<style>
|
||||
.data-table {
|
||||
background-color: #f8f9fa;
|
||||
/* Light grey background for consistency with other sections */
|
||||
padding: 20px;
|
||||
/* Padding around the DataTable for spacing */
|
||||
margin: 10px 0;
|
||||
/* Margin to separate it from other elements */
|
||||
border-radius: 5px;
|
||||
/* Rounded corners for the container */
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
/* Subtle shadow for depth */
|
||||
overflow-x: auto;
|
||||
/* Allows horizontal scrolling for overflow */
|
||||
}
|
||||
|
||||
table.display {
|
||||
width: 100%;
|
||||
/* Ensures the table fills its container */
|
||||
border-collapse: collapse;
|
||||
/* Neat borders */
|
||||
table-layout: fixed;
|
||||
/* Uniform column sizing */
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
/* Adequate padding for content */
|
||||
text-align: left;
|
||||
/* Align text to the left */
|
||||
border-bottom: 1px solid #ddd;
|
||||
/* Light grey border for each row */
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #e2e3e5;
|
||||
/* Lighter grey for headers to stand out */
|
||||
font-weight: bold;
|
||||
/* Bold text for headers */
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
/* Highlight for rows on hover */
|
||||
}
|
||||
|
||||
/* Dont use a list format for the fixed-in column */
|
||||
.fixed-in ul {
|
||||
list-style-type: none;
|
||||
/* Removes bullet points */
|
||||
padding: 0;
|
||||
/* Removes padding */
|
||||
margin: 0;
|
||||
/* Removes margin */
|
||||
}
|
||||
|
||||
.fixed-in li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<!-- Severity Coloring -->
|
||||
<style>
|
||||
.severity-pill {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
/* make the pill shape */
|
||||
color: white;
|
||||
/* Text color */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.critical {
|
||||
background-color: #d9534f;
|
||||
}
|
||||
|
||||
.high {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
||||
.medium {
|
||||
background-color: #5bc0de;
|
||||
}
|
||||
|
||||
.low {
|
||||
background-color: #5cb85c;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
background-color: #777;
|
||||
}
|
||||
</style>
|
||||
<!-- State Coloring -->
|
||||
<style>
|
||||
.state-pill {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
/* make the pill shape */
|
||||
color: white;
|
||||
/* Text color */
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
background-color: #d9534f;
|
||||
}
|
||||
|
||||
.not-fixed {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
.wont-fix {
|
||||
background-color: #5cb85c;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
{{/* Initialize counters */}}
|
||||
{{- $CountCritical := 0 }}
|
||||
{{- $CountHigh := 0 }}
|
||||
{{- $CountMedium := 0 }}
|
||||
{{- $CountLow := 0}}
|
||||
{{- $CountUnknown := 0 }}
|
||||
|
||||
{{/* Create a list */}}
|
||||
{{- $FilteredMatches := list }}
|
||||
|
||||
{{/* Loop through all vulns limit output and set count*/}}
|
||||
{{- range $vuln := .Matches }}
|
||||
{{/* Use this filter to exclude severity if needed */}}
|
||||
{{- if or (eq $vuln.Vulnerability.Severity "Critical") (eq $vuln.Vulnerability.Severity "High") (eq $vuln.Vulnerability.Severity "Medium") (eq $vuln.Vulnerability.Severity "Low") (eq $vuln.Vulnerability.Severity "Unknown") }}
|
||||
{{- $FilteredMatches = append $FilteredMatches $vuln }}
|
||||
{{- if eq $vuln.Vulnerability.Severity "Critical" }}
|
||||
{{- $CountCritical = add $CountCritical 1 }}
|
||||
{{- else if eq $vuln.Vulnerability.Severity "High" }}
|
||||
{{- $CountHigh = add $CountHigh 1 }}
|
||||
{{- else if eq $vuln.Vulnerability.Severity "Medium" }}
|
||||
{{- $CountMedium = add $CountMedium 1 }}
|
||||
{{- else if eq $vuln.Vulnerability.Severity "Low" }}
|
||||
{{- $CountLow = add $CountLow 1 }}
|
||||
{{- else }}
|
||||
{{- $CountUnknown = add $CountUnknown 1 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<div class="heading">
|
||||
<div class="heading-left">
|
||||
<h1>Container Vulnerability Report</h1>
|
||||
<p><strong>Name:</strong> {{- if eq (.Source.Type) "image" -}} {{.Source.Target.UserInput}}
|
||||
{{- else if eq (.Source.Type) "directory" -}} {{.Source.Target}}
|
||||
{{- else if eq (.Source.Type) "file" -}} {{.Source.Target}}
|
||||
{{- else -}} unknown
|
||||
{{- end -}}</p>
|
||||
<p><strong>Type:</strong> {{ .Source.Type }}</p>
|
||||
<p><strong>Date:</strong> <span id="dateElement">{{.Descriptor.Timestamp}}</span></p>
|
||||
</div>
|
||||
<div class="heading-right">
|
||||
<img src="https://user-images.githubusercontent.com/5199289/136855393-d0a9eef9-ccf1-4e2b-9d7c-7aad16a567e5.png"
|
||||
alt="Grype Logo">
|
||||
</div>
|
||||
</div>
|
||||
<div class="severity-info">
|
||||
<div class="severity-box" id="critical">
|
||||
<div class="severity-title">Critical</div>
|
||||
<div class="severity-count" id="criticalCount">{{ $CountCritical }}</div>
|
||||
</div>
|
||||
<div class="severity-box" id="high">
|
||||
<div class="severity-title">High</div>
|
||||
<div class="severity-count" id="highCount">{{ $CountHigh }}</div>
|
||||
</div>
|
||||
<div class="severity-box" id="medium">
|
||||
<div class="severity-title">Medium</div>
|
||||
<div class="severity-count" id="mediumCount">{{ $CountMedium }}</div>
|
||||
</div>
|
||||
<div class="severity-box" id="low">
|
||||
<div class="severity-title">Low</div>
|
||||
<div class="severity-count" id="lowCount">{{ $CountLow }}</div>
|
||||
</div>
|
||||
<div class="severity-box" id="unknown">
|
||||
<div class="severity-title">Unknown</div>
|
||||
<div class="severity-count" id="unknownCount">{{ $CountUnknown }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-table">
|
||||
<table id="vulnerabilityTable" class="display" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Version</th>
|
||||
<th>Type</th>
|
||||
<th>Vulnerability</th>
|
||||
<th>Severity</th>
|
||||
<th>Description</th>
|
||||
<th>State</th>
|
||||
<th>Fixed In</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{- range $FilteredMatches }}
|
||||
<tr>
|
||||
<td>{{.Artifact.Name}}</td>
|
||||
<td>{{.Artifact.Version}}</td>
|
||||
<td>{{.Artifact.Type}}</td>
|
||||
<td>
|
||||
<a href="{{.Vulnerability.DataSource}}">{{.Vulnerability.ID}}</a>
|
||||
</td>
|
||||
<td>{{.Vulnerability.Severity}}</td>
|
||||
<td>{{html .Vulnerability.Description}}</td>
|
||||
<td>{{.Vulnerability.Fix.State}}</td>
|
||||
<td>
|
||||
{{- if .Vulnerability.Fix.Versions }}
|
||||
<ul>
|
||||
{{- range .Vulnerability.Fix.Versions }}
|
||||
<li>{{ . }}</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
{{- else }}
|
||||
N/A
|
||||
{{- end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
initializeDataTable();
|
||||
prettyTimestamp();
|
||||
});
|
||||
|
||||
function prettyTimestamp() {
|
||||
var dateString = document.getElementById("dateElement").textContent;
|
||||
var date = new Date(dateString);
|
||||
|
||||
// Format date to a more human-readable form
|
||||
var formattedDate = date.toLocaleDateString("en-US", {
|
||||
weekday: "long", // "Monday"
|
||||
year: "numeric", // "2024"
|
||||
month: "long", // "April"
|
||||
day: "numeric", // "12"
|
||||
});
|
||||
|
||||
var formattedTime = date.toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: true, // Use AM/PM
|
||||
});
|
||||
|
||||
// Update the content of the div with formatted date and time
|
||||
document.getElementById("dateElement").textContent =
|
||||
formattedDate + " at " + formattedTime;
|
||||
}
|
||||
function initializeDataTable() {
|
||||
var table = $('#vulnerabilityTable').DataTable({
|
||||
responsive: true,
|
||||
dom: '<"dataTables_wrapper"Bf>t<"dataTables_control"ip>',
|
||||
buttons: [
|
||||
{ extend: 'pdfHtml5', text: 'Export to PDF' }
|
||||
],
|
||||
columnDefs: getColumnDefs()
|
||||
});
|
||||
|
||||
setupSeverityClickHandler(table);
|
||||
}
|
||||
|
||||
function getColumnDefs() {
|
||||
return [
|
||||
{
|
||||
targets: 5,
|
||||
className: 'none'
|
||||
},
|
||||
{
|
||||
targets: 4, // Index of 'Severity' Column
|
||||
render: function (data, type, row) {
|
||||
const severityClasses = {
|
||||
'Critical': 'critical',
|
||||
'High': 'high',
|
||||
'Medium': 'medium',
|
||||
'Low': 'low',
|
||||
'Unknown': 'unknown'
|
||||
};
|
||||
var className = severityClasses[data] || 'unknown';
|
||||
return '<span class="severity-pill ' + className + '">' + data + '</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
targets: 6, // Index of 'State' Column
|
||||
render: function (data, type, row) {
|
||||
const stateClasses = {
|
||||
'fixed': 'fixed',
|
||||
'not-fixed': 'not-fixed',
|
||||
'unknown': 'unknown',
|
||||
'wont-fix': 'wont-fix'
|
||||
};
|
||||
var className = stateClasses[data] || data; // Default to data if no match
|
||||
return '<span class="state-pill ' + className + '">' + data + '</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
targets: 7, // Index of 'Fixed In'
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
$(td).addClass('fixed-in'); // Adds a class to support styling
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function setupSeverityClickHandler(table) {
|
||||
var currentFilter = '';
|
||||
$('.severity-box').on('click', function () {
|
||||
var severity = $(this).find('.severity-title').text().trim();
|
||||
if (severity === currentFilter) {
|
||||
table.search('').columns().search('').draw();
|
||||
currentFilter = '';
|
||||
$('.severity-box').removeClass('active');
|
||||
} else {
|
||||
$('.severity-box').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
table.search('').columns().search('');
|
||||
table.column(4).search(severity).draw();
|
||||
currentFilter = severity;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in a new issue