diff --git a/api/src/reportcreator_api/conf/settings.py b/api/src/reportcreator_api/conf/settings.py index 022efa0a..4cff81fb 100644 --- a/api/src/reportcreator_api/conf/settings.py +++ b/api/src/reportcreator_api/conf/settings.py @@ -551,9 +551,9 @@ # Execute tasks locally, if no broker is configured CELERY_TASK_ALWAYS_EAGER = not CELERY_BROKER_URL -# Time limits are only enforced if a broker is configured and an external worker is used (but not in eager mode). -# Self-hosted SysReptor instances use the eager mode by default, resulting in no PDF rendering time limits being applied. -PDF_RENDERING_TIME_LIMIT = config('PDF_RENDERING_TIME_LIMIT', cast=int, default=60) +# Maximum time a PDF rendering task is allowed to run. If a task takes longer, it gets cancelled. +# Set to 0 to disable the time limit +PDF_RENDERING_TIME_LIMIT = config('PDF_RENDERING_TIME_LIMIT', cast=int, default=5 * 60) # History diff --git a/api/src/reportcreator_api/tasks/rendering/entry.py b/api/src/reportcreator_api/tasks/rendering/entry.py index 96589f21..0cf596ce 100644 --- a/api/src/reportcreator_api/tasks/rendering/entry.py +++ b/api/src/reportcreator_api/tasks/rendering/entry.py @@ -186,7 +186,8 @@ async def get_celery_result_async(task, timeout=None): async def _render_pdf_task_async(timeout=None, **kwargs): - timeout = timeout or timedelta(seconds=settings.PDF_RENDERING_TIME_LIMIT + 5) + if not timeout and settings.PDF_RENDERING_TIME_LIMIT: + timeout = timedelta(seconds=settings.PDF_RENDERING_TIME_LIMIT + 5) try: if settings.CELERY_TASK_ALWAYS_EAGER: @@ -224,7 +225,7 @@ def format_resources(): resources |= {'/images/name/' + i.name: b64encode(i.file.read()).decode() for i in project.images.all() if project.is_file_referenced(i, sections=True, findings=True, notes=False)} return resources - with res.add_timing('collect data'): + with res.add_timing('collect_data'): resources = await format_resources() timing_total_before = sum(res.timings.values()) @@ -325,7 +326,7 @@ async def render_note_to_pdf(note: Union[ProjectNotebookPage, UserNotebookPage], parent_obj = note.project if is_project_note else note.user res = RenderStageResult() - with res.add_timing('collect data'): + with res.add_timing('collect_data'): # Prevent sending unreferenced images to rendering task to reduce memory consumption resources = {} async for i in parent_obj.images.all(): @@ -343,7 +344,7 @@ async def render_note_to_pdf(note: Union[ProjectNotebookPage, UserNotebookPage], absolute_file_url = request.build_absolute_uri(reverse('uploadedusernotebookfile-retrieve-by-name', kwargs={'pentestuser_pk': note.user.id, 'filename': f.name})) note_text = note_text.replace(f'/files/name/{f.name}', absolute_file_url) - return await _render_pdf_task_async( + res |= await _render_pdf_task_async( template="""

{{ data.note.title }}

""", styles="""@import "/assets/global/base.css";""", data={ @@ -355,8 +356,8 @@ async def render_note_to_pdf(note: Union[ProjectNotebookPage, UserNotebookPage], }, language=note.project.language if is_project_note else Language.ENGLISH_US, resources=resources, - timings=res.timings, ) + return res async def render_pdf( @@ -373,7 +374,7 @@ async def render_pdf( res = RenderStageResult() - with res.add_timing('collect data'): + with res.add_timing('collect_data'): data = await format_project_template_data(project=project, project_type=project_type) return await render_pdf_task( project=project, @@ -389,7 +390,7 @@ async def render_pdf( async def render_pdf_preview(project_type: ProjectType, report_template: str, report_styles: str, report_preview_data: dict) -> RenderStageResult: res = RenderStageResult() - with res.add_timing('collect data'): + with res.add_timing('collect_data'): preview_data = report_preview_data.copy() data = await sync_to_async(format_template_data)(data=preview_data, project_type=project_type) diff --git a/api/src/reportcreator_api/tasks/rendering/render.py b/api/src/reportcreator_api/tasks/rendering/render.py index e3d86de1..86223721 100644 --- a/api/src/reportcreator_api/tasks/rendering/render.py +++ b/api/src/reportcreator_api/tasks/rendering/render.py @@ -110,7 +110,7 @@ def encrypt_pdf(pdf_data: bytes, password: Optional[str]) -> RenderStageResult: if not password: return out - with out.add_timing('compress_pdf'), \ + with out.add_timing('encrypt_pdf'), \ Pdf.open(BytesIO(pdf_data)) as pdf: out_data = BytesIO() # Encrypt PDF with AES-256 diff --git a/api/src/reportcreator_api/tasks/rendering/render_chromium.py b/api/src/reportcreator_api/tasks/rendering/render_chromium.py index 1485c862..ce6f64a6 100644 --- a/api/src/reportcreator_api/tasks/rendering/render_chromium.py +++ b/api/src/reportcreator_api/tasks/rendering/render_chromium.py @@ -57,12 +57,12 @@ async def chromium_request_handler(route): await route.abort() try: - chromium_startup_timer = out.add_timing('chromium startup') + chromium_startup_timer = out.add_timing('chromium_startup') chromium_startup_timer.__enter__() async with get_page() as page: chromium_startup_timer.__exit__(None, None, None) - with out.add_timing('chromium render'): + with out.add_timing('chromium_render'): console_output = [] page.on('console', lambda l: console_output.append(l)) page.on('pageerror', lambda exc: out.messages.append(ErrorMessage( diff --git a/api/src/reportcreator_api/tasks/rendering/tasks.py b/api/src/reportcreator_api/tasks/rendering/tasks.py index fcecb440..02a388cf 100644 --- a/api/src/reportcreator_api/tasks/rendering/tasks.py +++ b/api/src/reportcreator_api/tasks/rendering/tasks.py @@ -8,9 +8,9 @@ @shared_task( name='reportcreator.render_pdf', - soft_time_limit=settings.PDF_RENDERING_TIME_LIMIT, - time_limit=settings.PDF_RENDERING_TIME_LIMIT + 5, - expires=settings.PDF_RENDERING_TIME_LIMIT + 5, + soft_time_limit=settings.PDF_RENDERING_TIME_LIMIT or None, + time_limit=settings.PDF_RENDERING_TIME_LIMIT + 5 if settings.PDF_RENDERING_TIME_LIMIT else None, + expires=settings.PDF_RENDERING_TIME_LIMIT + 5 if settings.PDF_RENDERING_TIME_LIMIT else None, ) def render_pdf_task_celery(*args, **kwargs) -> dict: return asyncio.run(render_pdf_task_async(*args, **kwargs)) diff --git a/api/src/reportcreator_api/tests/test_rendering.py b/api/src/reportcreator_api/tests/test_rendering.py index ba82a595..9f9fb8e1 100644 --- a/api/src/reportcreator_api/tests/test_rendering.py +++ b/api/src/reportcreator_api/tests/test_rendering.py @@ -1,6 +1,5 @@ import io import re -from base64 import b64decode from unittest import mock import pikepdf @@ -121,8 +120,8 @@ def test_variables_rendering(self, template, html): def test_error_messages(self, template, expected): self.project_type.report_template = template res = async_to_sync(render_pdf)(project=self.project) - assert len(res['messages']) >= 1 - assert expected in [copy_keys(m, expected.keys()) for m in res['messages']] + assert len(res.messages) >= 1 + assert expected in [copy_keys(m.to_dict(), expected.keys()) for m in res.messages] def test_markdown_rendering(self): assertHTMLEqual( @@ -362,7 +361,7 @@ def test_mermaid_rendering(self): ('', False), ]) def test_pdf_encryption(self, password, encrypted): - pdf_data = b64decode(async_to_sync(render_pdf)(project=self.project, password=password)['pdf']) + pdf_data = async_to_sync(render_pdf)(project=self.project, password=password).pdf with pikepdf.Pdf.open(io.BytesIO(pdf_data), password=password) as pdf: assert pdf.is_encrypted == encrypted diff --git a/frontend/src/components/PdfPreview.vue b/frontend/src/components/PdfPreview.vue index 7d3fe25d..d630fcca 100644 --- a/frontend/src/components/PdfPreview.vue +++ b/frontend/src/components/PdfPreview.vue @@ -13,7 +13,7 @@ - {{ key }} + {{ key.replaceAll('_', ' ') }} {{ formatTiming(timing) }}