Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking: Simplify report generator and customization #125

Merged
merged 4 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
remove markdown export for advisories
  • Loading branch information
blockisec committed Jan 10, 2024
commit 8e9a77a94c518bfc16d8295784c13934264e5cd8
16 changes: 8 additions & 8 deletions frontend/src/utils/file.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
function forceFileDownload(response) {
const filename = filename_from_response(response);
const url = window.URL.createObjectURL(new Blob([response.data], { type: response.headers['content-type'] }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
const filename = filename_from_response(response);
const url = window.URL.createObjectURL(new Blob([response.data], { type: response.headers['content-type'] }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
}

function filename_from_response(response) {
return response.headers['content-disposition'].split('filename=')[1].split(';')[0];
return response.headers['content-disposition'].split('filename=')[1].split(';')[0];
}

export default forceFileDownload;
51 changes: 6 additions & 45 deletions frontend/src/views/pages/advisories/AdvisoryDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,6 @@ export default {
{ label: 'Medium', value: 'Medium' },
{ label: 'Low', value: 'Low' },
{ label: 'Informational', value: 'Informational' }
],
downloadMenuItems: [
{
label: 'Default PDF',
command: () => {
this.downloadAsPDF();
}
},
{
label: 'Default Markdown',
command: () => {
this.downloadAsMarkdown();
}
}
]
};
},
Expand Down Expand Up @@ -87,9 +73,6 @@ export default {
this.previewData = response.data;
});
},
toggleDownloadMenu(event) {
this.$refs.downloadMenu.toggle(event);
},
getAdvisory() {
this.service.getAdvisory(this.$api, this.advisoryId).then((response) => {
this.advisory = response.data;
Expand Down Expand Up @@ -134,36 +117,16 @@ export default {
};
this.patchAdvisory(data);
},
forceFileDownload(response, title) {
let blob = new Blob([response.data], { type: 'application/pdf' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', title);
link.click();
this.downloadPending = false;
},
forceMarkdownFileDownload(response, title) {
let blob = new Blob([response.data], { type: 'text/plain' });
forceFileDownload(response) {
let blob = new Blob([response.data], { type: response.headers['Content-Type'] });
let filename = response.headers['content-disposition'].split('filename=')[1].split(';')[0];
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', title);
link.setAttribute('download', filename);
link.click();
this.downloadPending = false;
},
downloadAsMarkdown() {
this.downloadPending = true;
this.service
.downloadAdvisoryAsMarkdown(this.$api, this.advisoryId)
.then((response) => {
const filename = 'advisory_' + this.advisoryId + '.md';
this.forceMarkdownFileDownload(response, filename);
})
.finally(() => {
this.downloadPending = false;
});
},
downloadAsPDF() {
this.downloadPending = true;
let params = {};
Expand All @@ -173,8 +136,7 @@ export default {
this.service
.downloadAdvisoryAsPDF(this.$api, this.advisoryId, params)
.then((response) => {
const filename = 'advisory_' + this.advisoryId + '.pdf';
this.forceFileDownload(response, filename);
this.forceFileDownload(response);
this.exportTemplate = null;
})
.finally(() => {
Expand Down Expand Up @@ -235,8 +197,7 @@ export default {
<div class="flex justify-content-end">
<Button icon="fa fa-eye" outlined label="Preview" @click="togglePreview"></Button>

<Button label="Download" icon="fa fa-download" outlined :loading="downloadPending" :disabled="downloadPending" @click="toggleDownloadMenu"></Button>
<Menu ref="downloadMenu" :model="downloadMenuItems" :popup="true"></Menu>
<Button label="Download" icon="fa fa-download" outlined :loading="downloadPending" :disabled="downloadPending" @click="downloadAsPDF"></Button>
<Button label="Edit" icon="fa fa-pen-to-square" outlined @click="this.$router.push({ name: 'AdvisoryUpdate', params: { advisoryId: this.advisoryId } })"></Button>
<Button label="Delete" severity="danger" @click="confirmDialogDelete" icon="fa fa-trash" outlined></Button>
</div>
Expand Down
45 changes: 0 additions & 45 deletions server/advisories/tests/test_advisory_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,48 +38,3 @@ def test_management_draft(self):
self.advisory1.save()
self.client.force_login(self.advisory_manager1)
self.basic_status_code_check(self.url, self.client.get, 403)


class AdvisoryMarkdownExportView(APITestCase, PeCoReTTestCaseMixin):
def setUp(self) -> None:
self.init_mixin()
self.url = self.get_url(
"advisories:advisory-export-markdown", pk=self.advisory1.pk
)

def test_allowed(self):
users = [
self.vendor1,
self.pentester1,
self.advisory_manager1,
self.read_only_vendor,
]
for user in users:
self.client.force_login(user)
self.basic_status_code_check(self.url, self.client.get, 200)

def test_forbidden(self):
users = [
self.management2,
self.management1,
self.user1,
self.vendor2,
self.read_only1,
self.pentester2,
]
for user in users:
self.client.force_login(user)
self.basic_status_code_check(self.url, self.client.get, 403)

def test_pentester1(self):
self.client.force_login(self.pentester1)
response = self.basic_status_code_check(self.url, self.client.get, 200)
self.assertNotEqual(response.headers.get("Content-Disposition"), None)
self.assertIn(self.advisory1.pk, response.headers["Content-Disposition"])

def test_advisory_management1_draft(self):
# test draft is not downloadable
self.advisory1.is_draft = True
self.advisory1.save()
self.client.force_login(self.advisory_manager1)
self.basic_status_code_check(self.url, self.client.get, 403)
11 changes: 1 addition & 10 deletions server/advisories/tests/test_advisory_export_tasks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework.test import APITestCase
from django_q.tasks import async_task, result
from backend import models
from backend.tasks.reporting import export_advisory, export_advisory_markdown
from backend.tasks.reporting import export_advisory
from pecoret.core.test import PeCoReTTestCaseMixin


Expand All @@ -23,12 +23,3 @@ def test_export_advisory(self):
)
task_result = result(task_id, 200)
self.assertIsNotNone(task_result)

def test_markdown_export(self):
"""test if markdown export of advisory works
"""
task_id = async_task(
export_advisory_markdown, self.advisory, self.report_template, sync=True
)
task_result = result(task_id, 200)
self.assertIsNotNone(task_result)
21 changes: 3 additions & 18 deletions server/advisories/viewsets/advisory.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
AdvisoryDownloadSerializer
)
from advisories.serializers.timeline import AdvisoryTimelineSerializer
from backend.tasks.reporting import export_advisory, export_advisory_markdown
from backend.tasks.reporting import export_advisory
from advisories.serializers.advisory_management import (
AdvisoryAdvisoryManagementSerializer, AdvisoryManagementUpdateSerializer
)
Expand Down Expand Up @@ -63,7 +63,7 @@ def get_permissions(self):
read_only_roles=[Roles.READ_ONLY, Roles.VENDOR],
)()
]
if self.action in ["export_pdf", "export_markdown"]:
if self.action in "export_pdf":
return [
permissions.AdvisoryPermission(
read_write_roles=[Roles.CREATOR],
Expand Down Expand Up @@ -142,25 +142,10 @@ def export_pdf(self, request, **kwargs):
advisory = self.get_object()
result = export_advisory(advisory, advisory.report_template)
response = HttpResponse(result, content_type="application/pdf")
filename = f"advisory_{advisory.pk}"
filename = f"advisory-{advisory.pk}"
response["Content-Disposition"] = f"attachment; filename={filename}.pdf"
return response

@action(detail=True, methods=["get"])
# pylint: disable=unused-argument
def export_markdown(self, _request, *args, **kwargs):
"""export advisory details as Markdown attachment

Returns:
HttpResponse: Response with file attachment
"""
advisory = self.get_object()
result = export_advisory_markdown(advisory, advisory.report_template)
response = HttpResponse(result, content_type="plain/text")
filename = f"advisory_{advisory.pk}.md"
response["Content-Disposition"] = f"attachment;filename={filename}"
return response

@action(detail=True, methods=["get"])
def preview(self, request, *args, **kwargs):
advisory = self.get_object()
Expand Down
8 changes: 0 additions & 8 deletions server/backend/tasks/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,3 @@ def export_advisory(advisory, template):
plugin = plugin_loader.load_plugin_from_report_template('export_advisory_pdf')
result, content_type, extension = plugin.export_advisory_pdf(advisory)
return result


def export_advisory_markdown(advisory, template):
plugin_loader = ReportPluginLoader(template)
plugin = plugin_loader.load_plugin_from_report_template('export_advisory_markdown')
result, content_type, extension = plugin.export_advisory_markdown(advisory)
return result

5 changes: 0 additions & 5 deletions server/extensions/report_templates/default_template/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,6 @@ def export_single_finding(self, finding):
generator = generators.PDFReportGenerator(self, context, language=finding.project.language)
return generator.generate('single_finding_export.html')

def export_advisory_markdown(self, advisory):
context = self.get_context(**{'advisory': advisory})
generator = generators.MarkdownGenerator(self, context)
return generator.generate('advisory.md')

def export_advisory_pdf(self, advisory):
context = self.get_context(**{'advisory': advisory, 'severity_colors': self.SEVERITY_COLORS})
generator = generators.PDFReportGenerator(self, context)
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion server/pecoret/reporting/generators/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from .pdf import PDFReportGenerator
from .markdown import MarkdownGenerator
from .excel import ExcelGenerator
from .csv import CSVGenerator
8 changes: 3 additions & 5 deletions server/pecoret/reporting/generators/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from django.utils import translation
from django.core.exceptions import ImproperlyConfigured
from jinja2.sandbox import SandboxedEnvironment
from jinja2 import PackageLoader
from jinja2 import FileSystemLoader
from pecoret.reporting.utils import dynamic_trans
from pecoret.core.utils import image64
from pecoret.core.utils.markdown import bleach_md


class BaseReportGenerator:
Expand All @@ -20,7 +18,7 @@ def __init__(self, report_plugin, context, language=None, preprocess_cb=None, po
self.report_plugin = report_plugin
self.preprocess_cb = preprocess_cb
self.postprocess_cb = postprocess_cb
self.jinja_loader = PackageLoader(self.report_plugin.template_package, 'templates')
self.jinja_loader = FileSystemLoader(self.report_plugin.template_directory)
self.jinja_env = SandboxedEnvironment(
loader=self.jinja_loader,
autoescape=self.jinja_autoescape,
Expand All @@ -47,7 +45,7 @@ def _postprocess(self, result):

:return: None
"""
self.report_plugin.on_postprocess(self, result)
return self.report_plugin.on_postprocess(self, result)

def generate(self, entry):
"""
Expand Down
2 changes: 1 addition & 1 deletion server/pecoret/reporting/generators/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ class CSVGenerator(ReportGenerator):
def generate(self, entry):
self._preprocess()
rendered = self.jinja_env.get_template(entry).render(self.context)
self._postprocess(rendered)
rendered = self._postprocess(rendered)
return rendered.encode(), self.content_type, self.file_extension
12 changes: 0 additions & 12 deletions server/pecoret/reporting/generators/markdown.py

This file was deleted.

18 changes: 11 additions & 7 deletions server/pecoret/reporting/report_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


class BaseReportPlugin:
template_name = None
plugin_name = None

def __init__(self, report_template):
self.report_template = report_template
Expand All @@ -22,24 +22,28 @@ def get_context(self, **kwargs):
kwargs.setdefault('now', datetime.datetime.now().strftime('%B %d, %Y'))
return kwargs

def get_template_name(self):
if self.template_name is None:
def get_plugin_name(self):
if self.plugin_name is None:
return self.report_template.name
return self.template_name
return self.plugin_name

@property
def template_package(self):
return f'extensions.report_templates.{self.get_template_name()}'
return f'extensions.report_templates.{self.get_plugin_name()}'

@property
def template_directory(self):
return Path(settings.EXTENSIONS_DIRECTORY / f'report_templates/{self.get_template_name()}/templates')
return Path(self.get_report_templates_directory() / f'{self.get_plugin_name()}/templates')

@staticmethod
def get_report_templates_directory():
return Path(settings.EXTENSIONS_DIRECTORY / 'report_templates')

def on_preprocess(self, generator, **kwargs):
pass

def on_postprocess(self, generator, result):
pass
return result


class ReportPluginLoader:
Expand Down