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
Next Next commit
first draft to simplify report generation
  • Loading branch information
blockisec committed Jan 9, 2024
commit 56db5b81b29cc62260b82bc15c9e501c30f3a906
38 changes: 16 additions & 22 deletions frontend/src/components/dialogs/ReportTemplateCreateDialog.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
<script>
import AdminService from '@/service/AdminService'

import AdminService from '@/service/AdminService';

export default {
name: "ReportTemplateCreateDialog",
emits: ["object-created"],
name: 'ReportTemplateCreateDialog',
emits: ['object-created'],
data() {
return {
visible: false,
model: {
name: null,
path: null,
status: null
},
statusChoices: [
{ title: "Active", value: "Active" },
{ title: "Draft", value: "Draft" },
{ title: "Deactivated", value: "Deactivated" }
{ title: 'Active', value: 'Active' },
{ title: 'Draft', value: 'Draft' },
{ title: 'Deactivated', value: 'Deactivated' }
],
service: new AdminService(),
service: new AdminService()
};
},
methods: {
Expand All @@ -33,20 +31,20 @@ export default {
name: this.model.name,
status: this.model.status,
path: this.model.path
}
};
this.service.createReportTemplate(this.$api, data).then((response) => {
this.$toast.add({
severity: "success",
summary: "Report template Created!",
severity: 'success',
summary: 'Report template Created!',
life: 3000,
detail: "Report template created successfully!"
detail: 'Report template created successfully!'
});
this.$emit("object-created", response.data);
this.$emit('object-created', response.data);
this.visible = false;
});
},
},
}
}
}
};
</script>

<template>
Expand All @@ -57,10 +55,6 @@ export default {
<label for="name">Name</label>
<InputText id="name" v-model="model.name"></InputText>
</div>
<div class="flex flex-column gap-2">
<label for="first_name">Path</label>
<InputText id="path" v-model="model.path"></InputText>
</div>
<div class="flex flex-column gap-2">
<label for="status">Status</label>
<Dropdown v-model="model.status" optionLabel="title" :options="statusChoices" optionValue="value"></Dropdown>
Expand All @@ -71,4 +65,4 @@ export default {
<Button label="Save" @click="create" icon="pi pi-check" class="p-button-outlined"></Button>
</template>
</Dialog>
</template>
</template>
29 changes: 11 additions & 18 deletions frontend/src/components/dialogs/ReportTemplateUpdateDialog.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
<script>
import AdminService from '@/service/AdminService'

import AdminService from '@/service/AdminService';

export default {
name: "ReportTemplateUpdateDialog",
name: 'ReportTemplateUpdateDialog',
props: {
template: {
required: true
}
},
emits: ["object-updated"],
emits: ['object-updated'],
data() {
return {
visible: false,
model: this.user,
service: new AdminService(),
statusChoices: [
{ title: "Active", value: "Active" },
{ title: "Draft", value: "Draft" },
{ title: "Deactivated", value: "Deactivated" }
{ title: 'Active', value: 'Active' },
{ title: 'Draft', value: 'Draft' },
{ title: 'Deactivated', value: 'Deactivated' }
]
};
},
Expand All @@ -32,14 +31,13 @@ export default {
patch() {
let data = {
name: this.model.name,
path: this.model.path,
status: this.model.status
}
};
this.service.patchReportTemplate(this.$api, this.template.pk, data).then(() => {
this.$emit("object-updated", this.model);
this.$emit('object-updated', this.model);
this.visible = false;
});
},
}
},
watch: {
template: {
Expand All @@ -49,23 +47,18 @@ export default {
this.model = value;
}
}
},
}
}
};
</script>

<template>
<Button icon="fa fa-pen-to-square" size="small" @click="open" outlined></Button>

<Dialog header="Update Report Template" v-model:visible="visible" :modal="true" :style="{ width: '70vw' }">

<div class="flex flex-column gap-2">
<label for="name">Name</label>
<InputText id="name" v-model="model.name"></InputText>
</div>
<div class="flex flex-column gap-2">
<label for="first_name">Path</label>
<InputText id="path" v-model="model.path"></InputText>
</div>
<div class="flex flex-column gap-2">
<label for="status">Status</label>
<Dropdown v-model="model.status" optionLabel="title" :options="statusChoices" optionValue="value"></Dropdown>
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/views/pages/admin/ReportTemplateList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,8 @@ export default {
<div class="col-12">
<Card>
<template #content>
<DataTable paginator dataKey rowHover :rows="pagination.limit" :value="items" lazy filterDisplay="menu" :totalRecords="totalRecords" :loading="loading" @page="onPage" responsiveLayout="scroll" @filter="onFilter" @sort="onSort">
<DataTable paginator rowHover :rows="pagination.limit" :value="items" lazy filterDisplay="menu" :totalRecords="totalRecords" :loading="loading" @page="onPage" responsiveLayout="scroll" @filter="onFilter" @sort="onSort">
<Column field="name" header="Name"></Column>
<Column field="path" header="Path"></Column>
<Column field="status" header="Status"></Column>
<Column header="Actions">
<template #body="slotProps">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export default {
this.findingService
.downloadAsPDF(this.$api, this.projectId, this.findingId)
.then((response) => {
const filename = 'finding_' + this.finding.internal_id + '.pdf';
const filename = 'finding-' + this.finding.unique_id.toLowerCase() + '.pdf';
this.forceFileDownload(response, filename);
})
.finally(() => {
Expand Down
2 changes: 1 addition & 1 deletion 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.finding_export import export_advisory, export_advisory_markdown
from backend.tasks.reporting import export_advisory, export_advisory_markdown
from pecoret.core.test import PeCoReTTestCaseMixin


Expand Down
4 changes: 1 addition & 3 deletions server/advisories/viewsets/advisory.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.http.response import HttpResponse
from django.conf import settings
from rest_framework.decorators import action
from backend.models.advisory import Advisory, Roles
from advisories.serializers.advisory import (
Expand All @@ -9,8 +8,7 @@
AdvisoryDownloadSerializer
)
from advisories.serializers.timeline import AdvisoryTimelineSerializer
from backend.models import ReportTemplate
from backend.tasks.finding_export import export_advisory, export_advisory_markdown
from backend.tasks.reporting import export_advisory, export_advisory_markdown
from advisories.serializers.advisory_management import (
AdvisoryAdvisoryManagementSerializer, AdvisoryManagementUpdateSerializer
)
Expand Down
3 changes: 0 additions & 3 deletions server/backend/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ class BackendConfig(AppConfig):
def ready(self):
# keep it here instead of the recommended pecoret.__init__
# otherwise gunicorn will fail to load because settings module is not yet ready
# pylint: disable=unused-import
# pylint: disable=import-outside-toplevel
import pecoret.schema
# patch translation
from pecoret.core.reporting.translation import patch_django_translation
patch_django_translation()
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.0 on 2024-01-09 05:07

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('backend', '0040_remove_finding_authenticated_test'),
]

operations = [
migrations.RemoveField(
model_name='reporttemplate',
name='path',
),
migrations.AlterField(
model_name='reporttemplate',
name='name',
field=models.CharField(max_length=64, unique=True, validators=[django.core.validators.RegexValidator('^[\\w\\_]*$')]),
),
]
11 changes: 6 additions & 5 deletions server/backend/models/report_templates.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from pathlib import Path
from django.db import models
from django.dispatch import receiver
from django.core.validators import RegexValidator
from django.core.exceptions import PermissionDenied, ValidationError
from django.conf import settings


class ReportTemplateStatus(models.IntegerChoices):
Expand All @@ -21,8 +22,7 @@ class ReportTemplate(models.Model):
date_updated = models.DateTimeField(auto_now=True)
status = models.PositiveSmallIntegerField(choices=ReportTemplateStatus.choices,
default=ReportTemplateStatus.ACTIVE)
name = models.CharField(max_length=64, unique=True)
path = models.CharField(max_length=128)
name = models.CharField(max_length=64, unique=True, validators=[RegexValidator(r'^[\w\_]*$')])

def __str__(self):
return self.name
Expand All @@ -45,9 +45,10 @@ def template_path(self):
"""the path of the ``templates`` subdirectory of the report template.

Returns:
str: path of the ``templates`` directory containing the jinja2 templates.
str: path of the ``templates`` directory containing the jinja2 templates.
"""
return str(Path(self.path) / "templates/")
path = f'{self.name}/templates'
return str(path)

class Meta:
ordering = ["-date_created", "name"]
Expand Down
10 changes: 7 additions & 3 deletions server/backend/models/reports/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ class ReportVariant(models.IntegerChoices):
PENTEST_EXCEL = 2, "Pentest Excel"

@property
def variant_report_class_name(self):
def to_plugin_method(self):
"""required to receive the required class of the variant

Returns:
str: class name (e.g. PentestPDFReport)
"""
class_name = self._label_.replace(" ", "")
return f"{class_name}Report"
mappers = {
'Pentest PDF': 'export_project_pdf_report',
'Vulnerability CSV': 'export_vulnerability_csv',
'Pentest Excel': 'export_project_excel'
}
return mappers[self._label_]


class ReportQuerySet(models.QuerySet):
Expand Down
1 change: 0 additions & 1 deletion server/backend/serializers/report_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,4 @@ class Meta:
fields = ReportTemplateMinimalSerializer.Meta.fields + [
"date_created",
"date_updated",
"path",
]
29 changes: 0 additions & 29 deletions server/backend/tasks/finding_export.py

This file was deleted.

35 changes: 30 additions & 5 deletions server/backend/tasks/reporting.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
from pecoret.reporting.report_plugin import ReportPluginLoader
from backend.models.reports.report_release import ReportRelease
from backend.models.reports.report import ReportVariant
from pecoret.core.reporting.loader import RenderableLoader


def create_report_document_task(report_document_pk):
report_document = ReportRelease.objects.get(pk=report_document_pk)
# get the report variant (e.g. Pentest PDF)
report_variant = ReportVariant(report_document.report.variant)
loader = RenderableLoader(report_template=report_document.report.template)
# pylint: disable=invalid-name
RenderableReport = loader.load_template_class_for_variant(report_variant)
result = RenderableReport(loader.report_template, report_document).generate()
plugin_loader = ReportPluginLoader(report_document.report.template)
plugin = plugin_loader.load_plugin_from_report_template(report_variant.to_plugin_method)
result, content_type, extension = getattr(plugin, report_variant.to_plugin_method)(report_document)
report_document.compiled_source = result
report_document.content_type = content_type
report_document.file_extension = extension
report_document.save()
return result


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


def export_advisory(advisory, template):
plugin_loader = ReportPluginLoader(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

2 changes: 1 addition & 1 deletion server/backend/tests/test_report_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from backend import models
from backend.models.reports.report_release import ReleaseType
from backend.tasks.reporting import create_report_document_task
from backend.tasks.finding_export import export_single_finding
from backend.tasks.reporting import export_single_finding
from pecoret.core.test import PeCoReTTestCaseMixin


Expand Down
2 changes: 1 addition & 1 deletion server/backend/viewsets/findings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
FindingAsAdvisorySerializer,
)
from backend.filters.finding import FindingFilter
from backend.tasks.finding_export import export_single_finding
from backend.tasks.reporting import export_single_finding
from backend.models.advisory import Advisory
from pecoret.core.viewsets import PeCoReTModelViewSet
from pecoret.core import permissions
Expand Down
Loading