mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-16 08:47:28 +02:00
add enable PDF generation setting
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -35,6 +35,7 @@ Phishing Club providers a lot of features for simulation and red teaming, here a
|
||||
- **Multiple domains** - Auto TLS, custom sites and asset management
|
||||
- **Advanced delivery** - SMTP configs or custom API Sender with OAuth support
|
||||
- **Recipient tracking** - Groups, CSV import, repeat offender metrics
|
||||
- **Campaign reports** - PDF export with a customizable HTML template
|
||||
- **Analytics** - Timelines, dashboards, per-user event history
|
||||
- **Automation** - HMAC-signed webhooks, REST API, import/export
|
||||
- **Multi-tenancy** - Segregated client handling and statistics for service providers
|
||||
|
||||
@@ -213,6 +213,7 @@ func NewControllers(
|
||||
Common: common,
|
||||
ReportTemplateService: services.ReportTemplate,
|
||||
CampaignService: services.Campaign,
|
||||
OptionService: services.Option,
|
||||
ExecPath: conf.RemoteBrowser.ExecPath,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/remotebrowser"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
@@ -16,6 +17,7 @@ type ReportTemplate struct {
|
||||
Common
|
||||
ReportTemplateService *service.ReportTemplate
|
||||
CampaignService *service.Campaign
|
||||
OptionService *service.Option
|
||||
ExecPath string
|
||||
}
|
||||
|
||||
@@ -121,6 +123,11 @@ func (r *ReportTemplate) GeneratePDFByCampaignID(g *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
opt, _ := r.OptionService.GetOptionWithoutAuth(g.Request.Context(), data.OptionKeyReportPDFEnabled)
|
||||
if opt == nil || opt.Value.String() != "true" {
|
||||
r.Response.NotFound(g)
|
||||
return
|
||||
}
|
||||
id, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
|
||||
@@ -37,6 +37,8 @@ const (
|
||||
|
||||
OptionKeyAutoPruneOrphanedRecipients = "auto_prune_orphaned_recipients"
|
||||
|
||||
OptionKeyReportPDFEnabled = "report_pdf_enabled"
|
||||
|
||||
OptionKeyObfuscationTemplate = "obfuscation_template"
|
||||
// OptionValueObfuscationTemplateDefault is the default HTML template for obfuscation
|
||||
// the template receives {{.Script}} variable containing the obfuscated javascript
|
||||
|
||||
@@ -426,6 +426,28 @@ func SeedSettings(
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
// seed report PDF enabled (disabled by default)
|
||||
id := uuid.New()
|
||||
var c int64
|
||||
res := db.
|
||||
Model(&database.Option{}).
|
||||
Where("key = ?", data.OptionKeyReportPDFEnabled).
|
||||
Count(&c)
|
||||
if res.Error != nil {
|
||||
return errs.Wrap(res.Error)
|
||||
}
|
||||
if c == 0 {
|
||||
res = db.Create(&database.Option{
|
||||
ID: &id,
|
||||
Key: data.OptionKeyReportPDFEnabled,
|
||||
Value: "false",
|
||||
})
|
||||
if res.Error != nil {
|
||||
return errs.Wrap(res.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,13 @@ func (o *Option) SetOptionByKey(
|
||||
"value",
|
||||
)
|
||||
}
|
||||
case data.OptionKeyReportPDFEnabled:
|
||||
if v != "true" && v != "false" {
|
||||
return validate.WrapErrorWithField(
|
||||
errs.NewValidationError(errors.New("invalid value")),
|
||||
"value",
|
||||
)
|
||||
}
|
||||
case data.OptionKeyObfuscationTemplate:
|
||||
// is allow listed
|
||||
default:
|
||||
|
||||
@@ -2302,7 +2302,7 @@ export class API {
|
||||
/**
|
||||
* Get setting by key.
|
||||
*
|
||||
* @param {'is_installed'|'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'} key
|
||||
* @param {'is_installed'|'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'|'report_pdf_enabled'} key
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
get: async (key) => {
|
||||
@@ -2333,7 +2333,7 @@ export class API {
|
||||
/**
|
||||
* Set setting by key and value.
|
||||
*
|
||||
* @param {'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'} key
|
||||
* @param {'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'|'report_pdf_enabled'} key
|
||||
* @param {string} value
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
let isReportedCSVModalVisible = false;
|
||||
let isReportedCSVSubmitting = false;
|
||||
let isGenerateReportModalVisible = false;
|
||||
let isReportPDFEnabled = false;
|
||||
let reportedCSVFile = null;
|
||||
let reportedCSVHeaders = [];
|
||||
let reportedCSVPreview = [];
|
||||
@@ -173,7 +174,7 @@
|
||||
let lastPoll3399Nano = '';
|
||||
|
||||
// live remote browser sessions
|
||||
/** @type {Map<string, {crID: string, campaignID: string, recipientID: string, createdAt: string, victimConnected: boolean}>} */
|
||||
/** @type {Map<string, {crID: string, campaignID: string, recipientID: string, createdAt: string, victimConnected: boolean, canStream: boolean}>} */
|
||||
let liveSessions = new Map();
|
||||
let liveSessionPollInterval = null;
|
||||
let streamModalVisible = false;
|
||||
@@ -191,6 +192,8 @@
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const pdfOpt = await api.option.get('report_pdf_enabled');
|
||||
isReportPDFEnabled = pdfOpt.success && pdfOpt.data?.value === 'true';
|
||||
await refresh();
|
||||
recipientTableUrlParams.onChange(refreshRecipients);
|
||||
eventsTableURLParams.onChange(refreshEvents);
|
||||
@@ -899,7 +902,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
let isReportPDFDisabledModalVisible = false;
|
||||
|
||||
const onClickGenerateReport = () => {
|
||||
if (!isReportPDFEnabled) {
|
||||
isReportPDFDisabledModalVisible = true;
|
||||
return;
|
||||
}
|
||||
isGenerateReportModalVisible = true;
|
||||
};
|
||||
|
||||
@@ -2898,4 +2907,18 @@
|
||||
automatically.
|
||||
</div>
|
||||
</Alert>
|
||||
<Alert
|
||||
headline="PDF reports disabled"
|
||||
bind:visible={isReportPDFDisabledModalVisible}
|
||||
onConfirm={() => {
|
||||
isReportPDFDisabledModalVisible = false;
|
||||
return { success: true };
|
||||
}}
|
||||
noCancel={true}
|
||||
ok="OK"
|
||||
>
|
||||
<div class="mt-4 text-gray-700 dark:text-gray-200">
|
||||
PDF report generation is not enabled. <br />Enable it in <strong>Settings</strong> under PDF Reports.
|
||||
</div>
|
||||
</Alert>
|
||||
</main>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import FormGrid from '$lib/components/FormGrid.svelte';
|
||||
import Headline from '$lib/components/Headline.svelte';
|
||||
import HeadTitle from '$lib/components/HeadTitle.svelte';
|
||||
import Alert from '$lib/components/Alert.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte';
|
||||
import PasswordField from '$lib/components/PasswordField.svelte';
|
||||
@@ -99,6 +100,9 @@
|
||||
let reportTemplateError = '';
|
||||
let isReportTemplateSubmitting = false;
|
||||
let isWipingBrowserCache = false;
|
||||
let isReportPDFEnabled = false;
|
||||
let isReportPDFEnableModalVisible = false;
|
||||
let isTogglingReportPDF = false;
|
||||
|
||||
$: {
|
||||
isCompanyContext = appState.isCompanyContext();
|
||||
@@ -123,6 +127,7 @@
|
||||
await refreshBackupList();
|
||||
await refreshDisplayMode();
|
||||
await refreshAutoPrune();
|
||||
await refreshReportPDFEnabled();
|
||||
if (!ssoSettingsFormValues.redirectURL) {
|
||||
ssoSettingsFormValues.redirectURL = `${location.origin}/api/v1/sso/entra-id/auth`;
|
||||
}
|
||||
@@ -605,6 +610,54 @@
|
||||
reportTemplateError = '';
|
||||
};
|
||||
|
||||
const refreshReportPDFEnabled = async () => {
|
||||
const response = await api.option.get('report_pdf_enabled');
|
||||
isReportPDFEnabled = response.success && response.data?.value === 'true';
|
||||
};
|
||||
|
||||
const onClickReportPDFToggle = () => {
|
||||
if (isReportPDFEnabled) {
|
||||
onDisableReportPDF();
|
||||
} else {
|
||||
isReportPDFEnableModalVisible = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onDisableReportPDF = async () => {
|
||||
isTogglingReportPDF = true;
|
||||
try {
|
||||
const response = await api.option.set('report_pdf_enabled', 'false');
|
||||
if (response.success) {
|
||||
isReportPDFEnabled = false;
|
||||
addToast('PDF reports disabled', 'Success');
|
||||
} else {
|
||||
addToast(response.error || 'Failed to update setting', 'Error');
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to update setting', 'Error');
|
||||
} finally {
|
||||
isTogglingReportPDF = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onConfirmEnableReportPDF = async () => {
|
||||
isTogglingReportPDF = true;
|
||||
try {
|
||||
const response = await api.option.set('report_pdf_enabled', 'true');
|
||||
if (response.success) {
|
||||
isReportPDFEnabled = true;
|
||||
isReportPDFEnableModalVisible = false;
|
||||
addToast('PDF reports enabled', 'Success');
|
||||
} else {
|
||||
addToast(response.error || 'Failed to update setting', 'Error');
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to update setting', 'Error');
|
||||
} finally {
|
||||
isTogglingReportPDF = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onWipeBrowserCache = async () => {
|
||||
isWipingBrowserCache = true;
|
||||
try {
|
||||
@@ -1062,6 +1115,37 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- PDF Reports Section -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 h-[420px] flex flex-col transition-colors duration-200"
|
||||
>
|
||||
<h2
|
||||
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
|
||||
>
|
||||
PDF Reports
|
||||
</h2>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="space-y-4">
|
||||
<p class="text-gray-600 dark:text-gray-300 text-sm transition-colors duration-200">
|
||||
Generate PDF reports for campaigns. Requires Chromium and system dependencies.
|
||||
</p>
|
||||
<p class="text-sm font-medium transition-colors duration-200" class:text-green-600={isReportPDFEnabled} class:dark:text-green-400={isReportPDFEnabled} class:text-gray-500={!isReportPDFEnabled} class:dark:text-gray-400={!isReportPDFEnabled}>
|
||||
{isReportPDFEnabled ? 'Enabled' : 'Disabled'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-auto pt-4">
|
||||
<Button
|
||||
size={'large'}
|
||||
backgroundColor={isReportPDFEnabled ? 'bg-red-600' : 'bg-cta-blue'}
|
||||
disabled={isTogglingReportPDF}
|
||||
on:click={onClickReportPDFToggle}
|
||||
>
|
||||
{isReportPDFEnabled ? 'Disable' : 'Enable'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Browser Cache Section -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 h-[420px] flex flex-col transition-colors duration-200"
|
||||
@@ -1552,4 +1636,18 @@
|
||||
</FormGrid>
|
||||
</Modal>
|
||||
{/if}
|
||||
{#if isReportPDFEnableModalVisible}
|
||||
<Alert
|
||||
headline="Enable PDF Reports"
|
||||
bind:visible={isReportPDFEnableModalVisible}
|
||||
onConfirm={onConfirmEnableReportPDF}
|
||||
ok="Enable"
|
||||
>
|
||||
<div class="mt-4 text-gray-700 dark:text-gray-200 space-y-3">
|
||||
<p>PDF report generation requires Chromium and additional system dependencies that are not part of the standard installation.</p>
|
||||
<p>Before enabling, ensure the host has the required libraries and any AppArmor restrictions on unprivileged user namespaces have been addressed.</p>
|
||||
<p>See <a href="https://phishing.club/guide/settings/#pdf-reports" target="_blank" class="underline">the setup guide</a> for dependency installation and AppArmor configuration.</p>
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user