Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions admin/css/webdecoy-admin.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1022,18 +1022,21 @@
white-space: nowrap;
}

.webdecoy-digest-status {
.webdecoy-digest-status,
.webdecoy-alerts-status {
display: flex;
align-items: center;
gap: 6px;
color: #3c4858;
margin: 16px 0 0;
}

.webdecoy-digest-status .dashicons {
.webdecoy-digest-status .dashicons,
.webdecoy-alerts-status .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
flex-shrink: 0;
}

.webdecoy-connected-actions {
Expand Down
26 changes: 25 additions & 1 deletion admin/partials/settings-page.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -804,6 +804,7 @@
$wd_plan_slug = isset($options['plan']) ? (string) $options['plan'] : '';
$wd_plan_label = WebDecoy_Cloud_Connect::plan_label($wd_plan_slug);
$wd_digest_on = !empty($wd_entitlements['digest']['enabled']);
$wd_alerts_on = !empty($wd_entitlements['features']['alerts']);
?>
<div class="webdecoy-connected-card">
<div class="webdecoy-connected-head">
Expand All@@ -826,6 +827,25 @@
?>
</p>

<?php
// Real-time alerts are configured in the dashboard, not here: the
// plugin sends nothing itself, which keeps the entitlement gating a
// cloud response rather than local behaviour. This row exists so a
// paying site can find the feature, and an unpaid one can see what
// it would get, without either having to guess.
?>
<p class="webdecoy-alerts-status">
<span class="dashicons <?php echo esc_attr($wd_alerts_on ? 'dashicons-bell' : 'dashicons-lock'); ?>"></span>
<?php if ($wd_alerts_on) : ?>
<?php esc_html_e('Slack and webhook alerts: On.', 'webdecoy'); ?>
<a href="https://app.webdecoy.com/enforcement/actions" target="_blank" rel="noopener">
<?php esc_html_e('Configure them in your dashboard', 'webdecoy'); ?>
</a>
<?php else : ?>
<?php esc_html_e('Slack and webhook alerts: available on Pro. Detection, blocking and the monthly report stay free.', 'webdecoy'); ?>
<?php endif; ?>
</p>

<p class="webdecoy-connected-actions">
<a href="<?php echo esc_url(WebDecoy_Cloud_Connect::wp_upgrade_url('wp_pro')); ?>" class="button button-primary" target="_blank" rel="noopener">
<?php esc_html_e('Upgrade', 'webdecoy'); ?>
Expand DownExpand Up@@ -856,7 +876,11 @@
<li><?php esc_html_e('VPN, proxy, and Tor exit node detection', 'webdecoy'); ?></li>
<li><?php esc_html_e('Cross-site threat intelligence from all WebDecoy users', 'webdecoy'); ?></li>
<li><?php esc_html_e('Advanced cloud analytics with indefinite data retention', 'webdecoy'); ?></li>
<li><?php esc_html_e('Webhook and email alert automation', 'webdecoy'); ?></li>
<?php // Not "email alerts": per-detection email is refused by the API
// (#702) because decoys are public bait and a busy site records
// thousands of hits a day. Email is the digest; webhooks are the
// real-time channel, configured in the dashboard. ?>
<li><?php esc_html_e('Webhook automation and a monthly email report', 'webdecoy'); ?></li>
</ul>
<p>
<a href="https://webdecoy.com/pricing" class="webdecoy-text-link" target="_blank" rel="noopener">
Expand Down
7 changes: 6 additions & 1 deletion includes/class-webdecoy-critical-moment.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,7 +217,12 @@ private function build_notice(string $ip): array
case 'unconnected':
default:
return [
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network — and to block threats like it automatically.', 'webdecoy'),
// Was "and to block threats like it automatically". Connecting does
// not block anything: the cross-site feed is advisory on every plan
// and writes nothing to the block list (#476). The unconnected pitch
// was the last place in this file still promising it, three variants
// below a comment forbidding exactly that claim.
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network, and what it has been doing to other sites.', 'webdecoy'),
'cta_label' => __('Connect to WebDecoy Cloud', 'webdecoy'),
'cta_url' => admin_url('admin.php?page=webdecoy&tab=cloud'),
'type' => 'warning',
Expand Down
8 changes: 4 additions & 4 deletions readme.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,16 +148,16 @@ And because **monitor mode is the default**, baking WebDecoy into your boilerpla

Connect an API key to unlock cloud-powered intelligence:

* **WAF Integrations**: arm your existing edge. Push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **WAF Integrations**: arm your existing edge. From your WebDecoy dashboard, push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **IP Reputation**: AbuseIPDB integration, threat scoring
* **VPN/Proxy Detection**: identify visitors hiding behind VPNs, proxies, and Tor
* **GeoIP Enrichment**: geographic data from MaxMind
* **Cloud Sync**: forward detections to a centralized dashboard
* **Cross-Site Intelligence**: aggregate threat data from all WebDecoy customers
* **Advanced Analytics**: cloud dashboard at app.webdecoy.com with indefinite history
* **Webhooks & Alerts**: automated response chains, email notifications
* **Webhooks & Alerts**: automated response chains, plus a monthly email report of what was caught

[Explore Plans](https://webdecoy.com/pricing) | [Start Free Trial](https://app.webdecoy.com/register)
[Explore Plans](https://webdecoy.com/pricing) | [Create a free account](https://app.webdecoy.com/register)

= Threat Scoring =

Expand DownExpand Up@@ -212,7 +212,7 @@ Firewalls match requests against known-bad signatures, and scanners look for inf

= What does WebDecoy Cloud add? =

WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, automated response features like webhooks and email alerts, and WAF integrations: confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).
WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, a monthly email report, webhook automation, and WAF integrations: from your dashboard, confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).

= Does WebDecoy slow down my site? =

Expand Down
40 changes: 40 additions & 0 deletions tests/CriticalMomentTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,43 @@
$same('connected_upgrade', WebDecoy_Critical_Moment::variant(true, true, false), 'known + no feed -> upgrade pitch');
$same('connected_covered', WebDecoy_Critical_Moment::variant(true, true, true), 'known + feed -> confirmation copy');
});

echo "\nCritical Moment: the copy must not promise a block\n";

/**
* The four message variants are built inside a method that calls WordPress, so
* they cannot be invoked here. Their text can still be read.
*
* That is worth doing because this exact claim has now been removed from this
* file twice. #476 measured the cross-site feed and withdrew blocking from it:
* 0.04% of addresses were ever seen at a second site, 82% of feed entries were
* already a week stale, and none were still active. The connected variants were
* corrected then, with a comment above them saying not to claim it. The
* unconnected variant kept promising "to block threats like it automatically"
* for another three weeks, four lines below that comment.
*
* A comment asking the next person not to do something is not a guard. This is.
*/
$t('no translatable string in the file offers to block anything', function () use ($true) {
$src = file_get_contents(dirname(__DIR__) . '/includes/class-webdecoy-critical-moment.php');
$true($src !== false, 'source is readable');

// Only the translated strings: comments in this file discuss blocking at
// length, and must be free to keep doing so.
preg_match_all("/(?:__|_e|esc_html__|esc_html_e)\(\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $singles);
preg_match_all("/_n\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $plurals);

$strings = array_merge($singles[1], $plurals[1], $plurals[2]);
$true(count($strings) >= 4, 'found the message strings (' . count($strings) . ')');

foreach ($strings as $text) {
$lower = strtolower($text);
foreach (['block', 'prevent', 'stop them'] as $promise) {
$true(
strpos($lower, $promise) === false,
'copy promises "' . $promise . '" — the feed is advisory on every plan and '
. 'writes nothing to the block list (#476): ' . $text
);
}
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions admin/css/webdecoy-admin.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1022,18 +1022,21 @@
white-space: nowrap;
}

.webdecoy-digest-status {
.webdecoy-digest-status,
.webdecoy-alerts-status {
display: flex;
align-items: center;
gap: 6px;
color: #3c4858;
margin: 16px 0 0;
}

.webdecoy-digest-status .dashicons {
.webdecoy-digest-status .dashicons,
.webdecoy-alerts-status .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
flex-shrink: 0;
}

.webdecoy-connected-actions {
Expand Down
26 changes: 25 additions & 1 deletion admin/partials/settings-page.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -804,6 +804,7 @@
$wd_plan_slug = isset($options['plan']) ? (string) $options['plan'] : '';
$wd_plan_label = WebDecoy_Cloud_Connect::plan_label($wd_plan_slug);
$wd_digest_on = !empty($wd_entitlements['digest']['enabled']);
$wd_alerts_on = !empty($wd_entitlements['features']['alerts']);
?>
<div class="webdecoy-connected-card">
<div class="webdecoy-connected-head">
Expand All@@ -826,6 +827,25 @@
?>
</p>

<?php
// Real-time alerts are configured in the dashboard, not here: the
// plugin sends nothing itself, which keeps the entitlement gating a
// cloud response rather than local behaviour. This row exists so a
// paying site can find the feature, and an unpaid one can see what
// it would get, without either having to guess.
?>
<p class="webdecoy-alerts-status">
<span class="dashicons <?php echo esc_attr($wd_alerts_on ? 'dashicons-bell' : 'dashicons-lock'); ?>"></span>
<?php if ($wd_alerts_on) : ?>
<?php esc_html_e('Slack and webhook alerts: On.', 'webdecoy'); ?>
<a href="https://app.webdecoy.com/enforcement/actions" target="_blank" rel="noopener">
<?php esc_html_e('Configure them in your dashboard', 'webdecoy'); ?>
</a>
<?php else : ?>
<?php esc_html_e('Slack and webhook alerts: available on Pro. Detection, blocking and the monthly report stay free.', 'webdecoy'); ?>
<?php endif; ?>
</p>

<p class="webdecoy-connected-actions">
<a href="<?php echo esc_url(WebDecoy_Cloud_Connect::wp_upgrade_url('wp_pro')); ?>" class="button button-primary" target="_blank" rel="noopener">
<?php esc_html_e('Upgrade', 'webdecoy'); ?>
Expand DownExpand Up@@ -856,7 +876,11 @@
<li><?php esc_html_e('VPN, proxy, and Tor exit node detection', 'webdecoy'); ?></li>
<li><?php esc_html_e('Cross-site threat intelligence from all WebDecoy users', 'webdecoy'); ?></li>
<li><?php esc_html_e('Advanced cloud analytics with indefinite data retention', 'webdecoy'); ?></li>
<li><?php esc_html_e('Webhook and email alert automation', 'webdecoy'); ?></li>
<?php // Not "email alerts": per-detection email is refused by the API
// (#702) because decoys are public bait and a busy site records
// thousands of hits a day. Email is the digest; webhooks are the
// real-time channel, configured in the dashboard. ?>
<li><?php esc_html_e('Webhook automation and a monthly email report', 'webdecoy'); ?></li>
</ul>
<p>
<a href="https://webdecoy.com/pricing" class="webdecoy-text-link" target="_blank" rel="noopener">
Expand Down
7 changes: 6 additions & 1 deletion includes/class-webdecoy-critical-moment.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,7 +217,12 @@ private function build_notice(string $ip): array
case 'unconnected':
default:
return [
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network — and to block threats like it automatically.', 'webdecoy'),
// Was "and to block threats like it automatically". Connecting does
// not block anything: the cross-site feed is advisory on every plan
// and writes nothing to the block list (#476). The unconnected pitch
// was the last place in this file still promising it, three variants
// below a comment forbidding exactly that claim.
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network, and what it has been doing to other sites.', 'webdecoy'),
'cta_label' => __('Connect to WebDecoy Cloud', 'webdecoy'),
'cta_url' => admin_url('admin.php?page=webdecoy&tab=cloud'),
'type' => 'warning',
Expand Down
8 changes: 4 additions & 4 deletions readme.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,16 +148,16 @@ And because **monitor mode is the default**, baking WebDecoy into your boilerpla

Connect an API key to unlock cloud-powered intelligence:

* **WAF Integrations**: arm your existing edge. Push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **WAF Integrations**: arm your existing edge. From your WebDecoy dashboard, push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **IP Reputation**: AbuseIPDB integration, threat scoring
* **VPN/Proxy Detection**: identify visitors hiding behind VPNs, proxies, and Tor
* **GeoIP Enrichment**: geographic data from MaxMind
* **Cloud Sync**: forward detections to a centralized dashboard
* **Cross-Site Intelligence**: aggregate threat data from all WebDecoy customers
* **Advanced Analytics**: cloud dashboard at app.webdecoy.com with indefinite history
* **Webhooks & Alerts**: automated response chains, email notifications
* **Webhooks & Alerts**: automated response chains, plus a monthly email report of what was caught

[Explore Plans](https://webdecoy.com/pricing) | [Start Free Trial](https://app.webdecoy.com/register)
[Explore Plans](https://webdecoy.com/pricing) | [Create a free account](https://app.webdecoy.com/register)

= Threat Scoring =

Expand DownExpand Up@@ -212,7 +212,7 @@ Firewalls match requests against known-bad signatures, and scanners look for inf

= What does WebDecoy Cloud add? =

WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, automated response features like webhooks and email alerts, and WAF integrations: confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).
WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, a monthly email report, webhook automation, and WAF integrations: from your dashboard, confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).

= Does WebDecoy slow down my site? =

Expand Down
40 changes: 40 additions & 0 deletions tests/CriticalMomentTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,43 @@
$same('connected_upgrade', WebDecoy_Critical_Moment::variant(true, true, false), 'known + no feed -> upgrade pitch');
$same('connected_covered', WebDecoy_Critical_Moment::variant(true, true, true), 'known + feed -> confirmation copy');
});

echo "\nCritical Moment: the copy must not promise a block\n";

/**
* The four message variants are built inside a method that calls WordPress, so
* they cannot be invoked here. Their text can still be read.
*
* That is worth doing because this exact claim has now been removed from this
* file twice. #476 measured the cross-site feed and withdrew blocking from it:
* 0.04% of addresses were ever seen at a second site, 82% of feed entries were
* already a week stale, and none were still active. The connected variants were
* corrected then, with a comment above them saying not to claim it. The
* unconnected variant kept promising "to block threats like it automatically"
* for another three weeks, four lines below that comment.
*
* A comment asking the next person not to do something is not a guard. This is.
*/
$t('no translatable string in the file offers to block anything', function () use ($true) {
$src = file_get_contents(dirname(__DIR__) . '/includes/class-webdecoy-critical-moment.php');
$true($src !== false, 'source is readable');

// Only the translated strings: comments in this file discuss blocking at
// length, and must be free to keep doing so.
preg_match_all("/(?:__|_e|esc_html__|esc_html_e)\(\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $singles);
preg_match_all("/_n\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $plurals);

$strings = array_merge($singles[1], $plurals[1], $plurals[2]);
$true(count($strings) >= 4, 'found the message strings (' . count($strings) . ')');

foreach ($strings as $text) {
$lower = strtolower($text);
foreach (['block', 'prevent', 'stop them'] as $promise) {
$true(
strpos($lower, $promise) === false,
'copy promises "' . $promise . '" — the feed is advisory on every plan and '
. 'writes nothing to the block list (#476): ' . $text
);
}
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions admin/css/webdecoy-admin.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1022,18 +1022,21 @@
white-space: nowrap;
}

.webdecoy-digest-status {
.webdecoy-digest-status,
.webdecoy-alerts-status {
display: flex;
align-items: center;
gap: 6px;
color: #3c4858;
margin: 16px 0 0;
}

.webdecoy-digest-status .dashicons {
.webdecoy-digest-status .dashicons,
.webdecoy-alerts-status .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
flex-shrink: 0;
}

.webdecoy-connected-actions {
Expand Down
26 changes: 25 additions & 1 deletion admin/partials/settings-page.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -804,6 +804,7 @@
$wd_plan_slug = isset($options['plan']) ? (string) $options['plan'] : '';
$wd_plan_label = WebDecoy_Cloud_Connect::plan_label($wd_plan_slug);
$wd_digest_on = !empty($wd_entitlements['digest']['enabled']);
$wd_alerts_on = !empty($wd_entitlements['features']['alerts']);
?>
<div class="webdecoy-connected-card">
<div class="webdecoy-connected-head">
Expand All@@ -826,6 +827,25 @@
?>
</p>

<?php
// Real-time alerts are configured in the dashboard, not here: the
// plugin sends nothing itself, which keeps the entitlement gating a
// cloud response rather than local behaviour. This row exists so a
// paying site can find the feature, and an unpaid one can see what
// it would get, without either having to guess.
?>
<p class="webdecoy-alerts-status">
<span class="dashicons <?php echo esc_attr($wd_alerts_on ? 'dashicons-bell' : 'dashicons-lock'); ?>"></span>
<?php if ($wd_alerts_on) : ?>
<?php esc_html_e('Slack and webhook alerts: On.', 'webdecoy'); ?>
<a href="https://app.webdecoy.com/enforcement/actions" target="_blank" rel="noopener">
<?php esc_html_e('Configure them in your dashboard', 'webdecoy'); ?>
</a>
<?php else : ?>
<?php esc_html_e('Slack and webhook alerts: available on Pro. Detection, blocking and the monthly report stay free.', 'webdecoy'); ?>
<?php endif; ?>
</p>

<p class="webdecoy-connected-actions">
<a href="<?php echo esc_url(WebDecoy_Cloud_Connect::wp_upgrade_url('wp_pro')); ?>" class="button button-primary" target="_blank" rel="noopener">
<?php esc_html_e('Upgrade', 'webdecoy'); ?>
Expand DownExpand Up@@ -856,7 +876,11 @@
<li><?php esc_html_e('VPN, proxy, and Tor exit node detection', 'webdecoy'); ?></li>
<li><?php esc_html_e('Cross-site threat intelligence from all WebDecoy users', 'webdecoy'); ?></li>
<li><?php esc_html_e('Advanced cloud analytics with indefinite data retention', 'webdecoy'); ?></li>
<li><?php esc_html_e('Webhook and email alert automation', 'webdecoy'); ?></li>
<?php // Not "email alerts": per-detection email is refused by the API
// (#702) because decoys are public bait and a busy site records
// thousands of hits a day. Email is the digest; webhooks are the
// real-time channel, configured in the dashboard. ?>
<li><?php esc_html_e('Webhook automation and a monthly email report', 'webdecoy'); ?></li>
</ul>
<p>
<a href="https://webdecoy.com/pricing" class="webdecoy-text-link" target="_blank" rel="noopener">
Expand Down
7 changes: 6 additions & 1 deletion includes/class-webdecoy-critical-moment.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,7 +217,12 @@ private function build_notice(string $ip): array
case 'unconnected':
default:
return [
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network — and to block threats like it automatically.', 'webdecoy'),
// Was "and to block threats like it automatically". Connecting does
// not block anything: the cross-site feed is advisory on every plan
// and writes nothing to the block list (#476). The unconnected pitch
// was the last place in this file still promising it, three variants
// below a comment forbidding exactly that claim.
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network, and what it has been doing to other sites.', 'webdecoy'),
'cta_label' => __('Connect to WebDecoy Cloud', 'webdecoy'),
'cta_url' => admin_url('admin.php?page=webdecoy&tab=cloud'),
'type' => 'warning',
Expand Down
8 changes: 4 additions & 4 deletions readme.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,16 +148,16 @@ And because **monitor mode is the default**, baking WebDecoy into your boilerpla

Connect an API key to unlock cloud-powered intelligence:

* **WAF Integrations**: arm your existing edge. Push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **WAF Integrations**: arm your existing edge. From your WebDecoy dashboard, push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **IP Reputation**: AbuseIPDB integration, threat scoring
* **VPN/Proxy Detection**: identify visitors hiding behind VPNs, proxies, and Tor
* **GeoIP Enrichment**: geographic data from MaxMind
* **Cloud Sync**: forward detections to a centralized dashboard
* **Cross-Site Intelligence**: aggregate threat data from all WebDecoy customers
* **Advanced Analytics**: cloud dashboard at app.webdecoy.com with indefinite history
* **Webhooks & Alerts**: automated response chains, email notifications
* **Webhooks & Alerts**: automated response chains, plus a monthly email report of what was caught

[Explore Plans](https://webdecoy.com/pricing) | [Start Free Trial](https://app.webdecoy.com/register)
[Explore Plans](https://webdecoy.com/pricing) | [Create a free account](https://app.webdecoy.com/register)

= Threat Scoring =

Expand DownExpand Up@@ -212,7 +212,7 @@ Firewalls match requests against known-bad signatures, and scanners look for inf

= What does WebDecoy Cloud add? =

WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, automated response features like webhooks and email alerts, and WAF integrations: confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).
WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, a monthly email report, webhook automation, and WAF integrations: from your dashboard, confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).

= Does WebDecoy slow down my site? =

Expand Down
40 changes: 40 additions & 0 deletions tests/CriticalMomentTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,43 @@
$same('connected_upgrade', WebDecoy_Critical_Moment::variant(true, true, false), 'known + no feed -> upgrade pitch');
$same('connected_covered', WebDecoy_Critical_Moment::variant(true, true, true), 'known + feed -> confirmation copy');
});

echo "\nCritical Moment: the copy must not promise a block\n";

/**
* The four message variants are built inside a method that calls WordPress, so
* they cannot be invoked here. Their text can still be read.
*
* That is worth doing because this exact claim has now been removed from this
* file twice. #476 measured the cross-site feed and withdrew blocking from it:
* 0.04% of addresses were ever seen at a second site, 82% of feed entries were
* already a week stale, and none were still active. The connected variants were
* corrected then, with a comment above them saying not to claim it. The
* unconnected variant kept promising "to block threats like it automatically"
* for another three weeks, four lines below that comment.
*
* A comment asking the next person not to do something is not a guard. This is.
*/
$t('no translatable string in the file offers to block anything', function () use ($true) {
$src = file_get_contents(dirname(__DIR__) . '/includes/class-webdecoy-critical-moment.php');
$true($src !== false, 'source is readable');

// Only the translated strings: comments in this file discuss blocking at
// length, and must be free to keep doing so.
preg_match_all("/(?:__|_e|esc_html__|esc_html_e)\(\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $singles);
preg_match_all("/_n\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $plurals);

$strings = array_merge($singles[1], $plurals[1], $plurals[2]);
$true(count($strings) >= 4, 'found the message strings (' . count($strings) . ')');

foreach ($strings as $text) {
$lower = strtolower($text);
foreach (['block', 'prevent', 'stop them'] as $promise) {
$true(
strpos($lower, $promise) === false,
'copy promises "' . $promise . '" — the feed is advisory on every plan and '
. 'writes nothing to the block list (#476): ' . $text
);
}
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions admin/css/webdecoy-admin.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1022,18 +1022,21 @@
white-space: nowrap;
}

.webdecoy-digest-status {
.webdecoy-digest-status,
.webdecoy-alerts-status {
display: flex;
align-items: center;
gap: 6px;
color: #3c4858;
margin: 16px 0 0;
}

.webdecoy-digest-status .dashicons {
.webdecoy-digest-status .dashicons,
.webdecoy-alerts-status .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
flex-shrink: 0;
}

.webdecoy-connected-actions {
Expand Down
26 changes: 25 additions & 1 deletion admin/partials/settings-page.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -804,6 +804,7 @@
$wd_plan_slug = isset($options['plan']) ? (string) $options['plan'] : '';
$wd_plan_label = WebDecoy_Cloud_Connect::plan_label($wd_plan_slug);
$wd_digest_on = !empty($wd_entitlements['digest']['enabled']);
$wd_alerts_on = !empty($wd_entitlements['features']['alerts']);
?>
<div class="webdecoy-connected-card">
<div class="webdecoy-connected-head">
Expand All@@ -826,6 +827,25 @@
?>
</p>

<?php
// Real-time alerts are configured in the dashboard, not here: the
// plugin sends nothing itself, which keeps the entitlement gating a
// cloud response rather than local behaviour. This row exists so a
// paying site can find the feature, and an unpaid one can see what
// it would get, without either having to guess.
?>
<p class="webdecoy-alerts-status">
<span class="dashicons <?php echo esc_attr($wd_alerts_on ? 'dashicons-bell' : 'dashicons-lock'); ?>"></span>
<?php if ($wd_alerts_on) : ?>
<?php esc_html_e('Slack and webhook alerts: On.', 'webdecoy'); ?>
<a href="https://app.webdecoy.com/enforcement/actions" target="_blank" rel="noopener">
<?php esc_html_e('Configure them in your dashboard', 'webdecoy'); ?>
</a>
<?php else : ?>
<?php esc_html_e('Slack and webhook alerts: available on Pro. Detection, blocking and the monthly report stay free.', 'webdecoy'); ?>
<?php endif; ?>
</p>

<p class="webdecoy-connected-actions">
<a href="<?php echo esc_url(WebDecoy_Cloud_Connect::wp_upgrade_url('wp_pro')); ?>" class="button button-primary" target="_blank" rel="noopener">
<?php esc_html_e('Upgrade', 'webdecoy'); ?>
Expand DownExpand Up@@ -856,7 +876,11 @@
<li><?php esc_html_e('VPN, proxy, and Tor exit node detection', 'webdecoy'); ?></li>
<li><?php esc_html_e('Cross-site threat intelligence from all WebDecoy users', 'webdecoy'); ?></li>
<li><?php esc_html_e('Advanced cloud analytics with indefinite data retention', 'webdecoy'); ?></li>
<li><?php esc_html_e('Webhook and email alert automation', 'webdecoy'); ?></li>
<?php // Not "email alerts": per-detection email is refused by the API
// (#702) because decoys are public bait and a busy site records
// thousands of hits a day. Email is the digest; webhooks are the
// real-time channel, configured in the dashboard. ?>
<li><?php esc_html_e('Webhook automation and a monthly email report', 'webdecoy'); ?></li>
</ul>
<p>
<a href="https://webdecoy.com/pricing" class="webdecoy-text-link" target="_blank" rel="noopener">
Expand Down
7 changes: 6 additions & 1 deletion includes/class-webdecoy-critical-moment.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,7 +217,12 @@ private function build_notice(string $ip): array
case 'unconnected':
default:
return [
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network — and to block threats like it automatically.', 'webdecoy'),
// Was "and to block threats like it automatically". Connecting does
// not block anything: the cross-site feed is advisory on every plan
// and writes nothing to the block list (#476). The unconnected pitch
// was the last place in this file still promising it, three variants
// below a comment forbidding exactly that claim.
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network, and what it has been doing to other sites.', 'webdecoy'),
'cta_label' => __('Connect to WebDecoy Cloud', 'webdecoy'),
'cta_url' => admin_url('admin.php?page=webdecoy&tab=cloud'),
'type' => 'warning',
Expand Down
8 changes: 4 additions & 4 deletions readme.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,16 +148,16 @@ And because **monitor mode is the default**, baking WebDecoy into your boilerpla

Connect an API key to unlock cloud-powered intelligence:

* **WAF Integrations**: arm your existing edge. Push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **WAF Integrations**: arm your existing edge. From your WebDecoy dashboard, push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **IP Reputation**: AbuseIPDB integration, threat scoring
* **VPN/Proxy Detection**: identify visitors hiding behind VPNs, proxies, and Tor
* **GeoIP Enrichment**: geographic data from MaxMind
* **Cloud Sync**: forward detections to a centralized dashboard
* **Cross-Site Intelligence**: aggregate threat data from all WebDecoy customers
* **Advanced Analytics**: cloud dashboard at app.webdecoy.com with indefinite history
* **Webhooks & Alerts**: automated response chains, email notifications
* **Webhooks & Alerts**: automated response chains, plus a monthly email report of what was caught

[Explore Plans](https://webdecoy.com/pricing) | [Start Free Trial](https://app.webdecoy.com/register)
[Explore Plans](https://webdecoy.com/pricing) | [Create a free account](https://app.webdecoy.com/register)

= Threat Scoring =

Expand DownExpand Up@@ -212,7 +212,7 @@ Firewalls match requests against known-bad signatures, and scanners look for inf

= What does WebDecoy Cloud add? =

WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, automated response features like webhooks and email alerts, and WAF integrations: confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).
WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, a monthly email report, webhook automation, and WAF integrations: from your dashboard, confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).

= Does WebDecoy slow down my site? =

Expand Down
40 changes: 40 additions & 0 deletions tests/CriticalMomentTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,43 @@
$same('connected_upgrade', WebDecoy_Critical_Moment::variant(true, true, false), 'known + no feed -> upgrade pitch');
$same('connected_covered', WebDecoy_Critical_Moment::variant(true, true, true), 'known + feed -> confirmation copy');
});

echo "\nCritical Moment: the copy must not promise a block\n";

/**
* The four message variants are built inside a method that calls WordPress, so
* they cannot be invoked here. Their text can still be read.
*
* That is worth doing because this exact claim has now been removed from this
* file twice. #476 measured the cross-site feed and withdrew blocking from it:
* 0.04% of addresses were ever seen at a second site, 82% of feed entries were
* already a week stale, and none were still active. The connected variants were
* corrected then, with a comment above them saying not to claim it. The
* unconnected variant kept promising "to block threats like it automatically"
* for another three weeks, four lines below that comment.
*
* A comment asking the next person not to do something is not a guard. This is.
*/
$t('no translatable string in the file offers to block anything', function () use ($true) {
$src = file_get_contents(dirname(__DIR__) . '/includes/class-webdecoy-critical-moment.php');
$true($src !== false, 'source is readable');

// Only the translated strings: comments in this file discuss blocking at
// length, and must be free to keep doing so.
preg_match_all("/(?:__|_e|esc_html__|esc_html_e)\(\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $singles);
preg_match_all("/_n\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $plurals);

$strings = array_merge($singles[1], $plurals[1], $plurals[2]);
$true(count($strings) >= 4, 'found the message strings (' . count($strings) . ')');

foreach ($strings as $text) {
$lower = strtolower($text);
foreach (['block', 'prevent', 'stop them'] as $promise) {
$true(
strpos($lower, $promise) === false,
'copy promises "' . $promise . '" — the feed is advisory on every plan and '
. 'writes nothing to the block list (#476): ' . $text
);
}
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions admin/css/webdecoy-admin.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1022,18 +1022,21 @@
white-space: nowrap;
}

.webdecoy-digest-status {
.webdecoy-digest-status,
.webdecoy-alerts-status {
display: flex;
align-items: center;
gap: 6px;
color: #3c4858;
margin: 16px 0 0;
}

.webdecoy-digest-status .dashicons {
.webdecoy-digest-status .dashicons,
.webdecoy-alerts-status .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
flex-shrink: 0;
}

.webdecoy-connected-actions {
Expand Down
26 changes: 25 additions & 1 deletion admin/partials/settings-page.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -804,6 +804,7 @@
$wd_plan_slug = isset($options['plan']) ? (string) $options['plan'] : '';
$wd_plan_label = WebDecoy_Cloud_Connect::plan_label($wd_plan_slug);
$wd_digest_on = !empty($wd_entitlements['digest']['enabled']);
$wd_alerts_on = !empty($wd_entitlements['features']['alerts']);
?>
<div class="webdecoy-connected-card">
<div class="webdecoy-connected-head">
Expand All@@ -826,6 +827,25 @@
?>
</p>

<?php
// Real-time alerts are configured in the dashboard, not here: the
// plugin sends nothing itself, which keeps the entitlement gating a
// cloud response rather than local behaviour. This row exists so a
// paying site can find the feature, and an unpaid one can see what
// it would get, without either having to guess.
?>
<p class="webdecoy-alerts-status">
<span class="dashicons <?php echo esc_attr($wd_alerts_on ? 'dashicons-bell' : 'dashicons-lock'); ?>"></span>
<?php if ($wd_alerts_on) : ?>
<?php esc_html_e('Slack and webhook alerts: On.', 'webdecoy'); ?>
<a href="https://app.webdecoy.com/enforcement/actions" target="_blank" rel="noopener">
<?php esc_html_e('Configure them in your dashboard', 'webdecoy'); ?>
</a>
<?php else : ?>
<?php esc_html_e('Slack and webhook alerts: available on Pro. Detection, blocking and the monthly report stay free.', 'webdecoy'); ?>
<?php endif; ?>
</p>

<p class="webdecoy-connected-actions">
<a href="<?php echo esc_url(WebDecoy_Cloud_Connect::wp_upgrade_url('wp_pro')); ?>" class="button button-primary" target="_blank" rel="noopener">
<?php esc_html_e('Upgrade', 'webdecoy'); ?>
Expand DownExpand Up@@ -856,7 +876,11 @@
<li><?php esc_html_e('VPN, proxy, and Tor exit node detection', 'webdecoy'); ?></li>
<li><?php esc_html_e('Cross-site threat intelligence from all WebDecoy users', 'webdecoy'); ?></li>
<li><?php esc_html_e('Advanced cloud analytics with indefinite data retention', 'webdecoy'); ?></li>
<li><?php esc_html_e('Webhook and email alert automation', 'webdecoy'); ?></li>
<?php // Not "email alerts": per-detection email is refused by the API
// (#702) because decoys are public bait and a busy site records
// thousands of hits a day. Email is the digest; webhooks are the
// real-time channel, configured in the dashboard. ?>
<li><?php esc_html_e('Webhook automation and a monthly email report', 'webdecoy'); ?></li>
</ul>
<p>
<a href="https://webdecoy.com/pricing" class="webdecoy-text-link" target="_blank" rel="noopener">
Expand Down
7 changes: 6 additions & 1 deletion includes/class-webdecoy-critical-moment.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,7 +217,12 @@ private function build_notice(string $ip): array
case 'unconnected':
default:
return [
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network — and to block threats like it automatically.', 'webdecoy'),
// Was "and to block threats like it automatically". Connecting does
// not block anything: the cross-site feed is advisory on every plan
// and writes nothing to the block list (#476). The unconnected pitch
// was the last place in this file still promising it, three variants
// below a comment forbidding exactly that claim.
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network, and what it has been doing to other sites.', 'webdecoy'),
'cta_label' => __('Connect to WebDecoy Cloud', 'webdecoy'),
'cta_url' => admin_url('admin.php?page=webdecoy&tab=cloud'),
'type' => 'warning',
Expand Down
8 changes: 4 additions & 4 deletions readme.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,16 +148,16 @@ And because **monitor mode is the default**, baking WebDecoy into your boilerpla

Connect an API key to unlock cloud-powered intelligence:

* **WAF Integrations**: arm your existing edge. Push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **WAF Integrations**: arm your existing edge. From your WebDecoy dashboard, push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **IP Reputation**: AbuseIPDB integration, threat scoring
* **VPN/Proxy Detection**: identify visitors hiding behind VPNs, proxies, and Tor
* **GeoIP Enrichment**: geographic data from MaxMind
* **Cloud Sync**: forward detections to a centralized dashboard
* **Cross-Site Intelligence**: aggregate threat data from all WebDecoy customers
* **Advanced Analytics**: cloud dashboard at app.webdecoy.com with indefinite history
* **Webhooks & Alerts**: automated response chains, email notifications
* **Webhooks & Alerts**: automated response chains, plus a monthly email report of what was caught

[Explore Plans](https://webdecoy.com/pricing) | [Start Free Trial](https://app.webdecoy.com/register)
[Explore Plans](https://webdecoy.com/pricing) | [Create a free account](https://app.webdecoy.com/register)

= Threat Scoring =

Expand DownExpand Up@@ -212,7 +212,7 @@ Firewalls match requests against known-bad signatures, and scanners look for inf

= What does WebDecoy Cloud add? =

WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, automated response features like webhooks and email alerts, and WAF integrations: confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).
WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, a monthly email report, webhook automation, and WAF integrations: from your dashboard, confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).

= Does WebDecoy slow down my site? =

Expand Down
40 changes: 40 additions & 0 deletions tests/CriticalMomentTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,43 @@
$same('connected_upgrade', WebDecoy_Critical_Moment::variant(true, true, false), 'known + no feed -> upgrade pitch');
$same('connected_covered', WebDecoy_Critical_Moment::variant(true, true, true), 'known + feed -> confirmation copy');
});

echo "\nCritical Moment: the copy must not promise a block\n";

/**
* The four message variants are built inside a method that calls WordPress, so
* they cannot be invoked here. Their text can still be read.
*
* That is worth doing because this exact claim has now been removed from this
* file twice. #476 measured the cross-site feed and withdrew blocking from it:
* 0.04% of addresses were ever seen at a second site, 82% of feed entries were
* already a week stale, and none were still active. The connected variants were
* corrected then, with a comment above them saying not to claim it. The
* unconnected variant kept promising "to block threats like it automatically"
* for another three weeks, four lines below that comment.
*
* A comment asking the next person not to do something is not a guard. This is.
*/
$t('no translatable string in the file offers to block anything', function () use ($true) {
$src = file_get_contents(dirname(__DIR__) . '/includes/class-webdecoy-critical-moment.php');
$true($src !== false, 'source is readable');

// Only the translated strings: comments in this file discuss blocking at
// length, and must be free to keep doing so.
preg_match_all("/(?:__|_e|esc_html__|esc_html_e)\(\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $singles);
preg_match_all("/_n\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $plurals);

$strings = array_merge($singles[1], $plurals[1], $plurals[2]);
$true(count($strings) >= 4, 'found the message strings (' . count($strings) . ')');

foreach ($strings as $text) {
$lower = strtolower($text);
foreach (['block', 'prevent', 'stop them'] as $promise) {
$true(
strpos($lower, $promise) === false,
'copy promises "' . $promise . '" — the feed is advisory on every plan and '
. 'writes nothing to the block list (#476): ' . $text
);
}
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions admin/css/webdecoy-admin.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1022,18 +1022,21 @@
white-space: nowrap;
}

.webdecoy-digest-status {
.webdecoy-digest-status,
.webdecoy-alerts-status {
display: flex;
align-items: center;
gap: 6px;
color: #3c4858;
margin: 16px 0 0;
}

.webdecoy-digest-status .dashicons {
.webdecoy-digest-status .dashicons,
.webdecoy-alerts-status .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
flex-shrink: 0;
}

.webdecoy-connected-actions {
Expand Down
26 changes: 25 additions & 1 deletion admin/partials/settings-page.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -804,6 +804,7 @@
$wd_plan_slug = isset($options['plan']) ? (string) $options['plan'] : '';
$wd_plan_label = WebDecoy_Cloud_Connect::plan_label($wd_plan_slug);
$wd_digest_on = !empty($wd_entitlements['digest']['enabled']);
$wd_alerts_on = !empty($wd_entitlements['features']['alerts']);
?>
<div class="webdecoy-connected-card">
<div class="webdecoy-connected-head">
Expand All@@ -826,6 +827,25 @@
?>
</p>

<?php
// Real-time alerts are configured in the dashboard, not here: the
// plugin sends nothing itself, which keeps the entitlement gating a
// cloud response rather than local behaviour. This row exists so a
// paying site can find the feature, and an unpaid one can see what
// it would get, without either having to guess.
?>
<p class="webdecoy-alerts-status">
<span class="dashicons <?php echo esc_attr($wd_alerts_on ? 'dashicons-bell' : 'dashicons-lock'); ?>"></span>
<?php if ($wd_alerts_on) : ?>
<?php esc_html_e('Slack and webhook alerts: On.', 'webdecoy'); ?>
<a href="https://app.webdecoy.com/enforcement/actions" target="_blank" rel="noopener">
<?php esc_html_e('Configure them in your dashboard', 'webdecoy'); ?>
</a>
<?php else : ?>
<?php esc_html_e('Slack and webhook alerts: available on Pro. Detection, blocking and the monthly report stay free.', 'webdecoy'); ?>
<?php endif; ?>
</p>

<p class="webdecoy-connected-actions">
<a href="<?php echo esc_url(WebDecoy_Cloud_Connect::wp_upgrade_url('wp_pro')); ?>" class="button button-primary" target="_blank" rel="noopener">
<?php esc_html_e('Upgrade', 'webdecoy'); ?>
Expand DownExpand Up@@ -856,7 +876,11 @@
<li><?php esc_html_e('VPN, proxy, and Tor exit node detection', 'webdecoy'); ?></li>
<li><?php esc_html_e('Cross-site threat intelligence from all WebDecoy users', 'webdecoy'); ?></li>
<li><?php esc_html_e('Advanced cloud analytics with indefinite data retention', 'webdecoy'); ?></li>
<li><?php esc_html_e('Webhook and email alert automation', 'webdecoy'); ?></li>
<?php // Not "email alerts": per-detection email is refused by the API
// (#702) because decoys are public bait and a busy site records
// thousands of hits a day. Email is the digest; webhooks are the
// real-time channel, configured in the dashboard. ?>
<li><?php esc_html_e('Webhook automation and a monthly email report', 'webdecoy'); ?></li>
</ul>
<p>
<a href="https://webdecoy.com/pricing" class="webdecoy-text-link" target="_blank" rel="noopener">
Expand Down
7 changes: 6 additions & 1 deletion includes/class-webdecoy-critical-moment.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,7 +217,12 @@ private function build_notice(string $ip): array
case 'unconnected':
default:
return [
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network — and to block threats like it automatically.', 'webdecoy'),
// Was "and to block threats like it automatically". Connecting does
// not block anything: the cross-site feed is advisory on every plan
// and writes nothing to the block list (#476). The unconnected pitch
// was the last place in this file still promising it, three variants
// below a comment forbidding exactly that claim.
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network, and what it has been doing to other sites.', 'webdecoy'),
'cta_label' => __('Connect to WebDecoy Cloud', 'webdecoy'),
'cta_url' => admin_url('admin.php?page=webdecoy&tab=cloud'),
'type' => 'warning',
Expand Down
8 changes: 4 additions & 4 deletions readme.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,16 +148,16 @@ And because **monitor mode is the default**, baking WebDecoy into your boilerpla

Connect an API key to unlock cloud-powered intelligence:

* **WAF Integrations**: arm your existing edge. Push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **WAF Integrations**: arm your existing edge. From your WebDecoy dashboard, push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **IP Reputation**: AbuseIPDB integration, threat scoring
* **VPN/Proxy Detection**: identify visitors hiding behind VPNs, proxies, and Tor
* **GeoIP Enrichment**: geographic data from MaxMind
* **Cloud Sync**: forward detections to a centralized dashboard
* **Cross-Site Intelligence**: aggregate threat data from all WebDecoy customers
* **Advanced Analytics**: cloud dashboard at app.webdecoy.com with indefinite history
* **Webhooks & Alerts**: automated response chains, email notifications
* **Webhooks & Alerts**: automated response chains, plus a monthly email report of what was caught

[Explore Plans](https://webdecoy.com/pricing) | [Start Free Trial](https://app.webdecoy.com/register)
[Explore Plans](https://webdecoy.com/pricing) | [Create a free account](https://app.webdecoy.com/register)

= Threat Scoring =

Expand DownExpand Up@@ -212,7 +212,7 @@ Firewalls match requests against known-bad signatures, and scanners look for inf

= What does WebDecoy Cloud add? =

WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, automated response features like webhooks and email alerts, and WAF integrations: confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).
WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, a monthly email report, webhook automation, and WAF integrations: from your dashboard, confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).

= Does WebDecoy slow down my site? =

Expand Down
40 changes: 40 additions & 0 deletions tests/CriticalMomentTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,43 @@
$same('connected_upgrade', WebDecoy_Critical_Moment::variant(true, true, false), 'known + no feed -> upgrade pitch');
$same('connected_covered', WebDecoy_Critical_Moment::variant(true, true, true), 'known + feed -> confirmation copy');
});

echo "\nCritical Moment: the copy must not promise a block\n";

/**
* The four message variants are built inside a method that calls WordPress, so
* they cannot be invoked here. Their text can still be read.
*
* That is worth doing because this exact claim has now been removed from this
* file twice. #476 measured the cross-site feed and withdrew blocking from it:
* 0.04% of addresses were ever seen at a second site, 82% of feed entries were
* already a week stale, and none were still active. The connected variants were
* corrected then, with a comment above them saying not to claim it. The
* unconnected variant kept promising "to block threats like it automatically"
* for another three weeks, four lines below that comment.
*
* A comment asking the next person not to do something is not a guard. This is.
*/
$t('no translatable string in the file offers to block anything', function () use ($true) {
$src = file_get_contents(dirname(__DIR__) . '/includes/class-webdecoy-critical-moment.php');
$true($src !== false, 'source is readable');

// Only the translated strings: comments in this file discuss blocking at
// length, and must be free to keep doing so.
preg_match_all("/(?:__|_e|esc_html__|esc_html_e)\(\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $singles);
preg_match_all("/_n\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $plurals);

$strings = array_merge($singles[1], $plurals[1], $plurals[2]);
$true(count($strings) >= 4, 'found the message strings (' . count($strings) . ')');

foreach ($strings as $text) {
$lower = strtolower($text);
foreach (['block', 'prevent', 'stop them'] as $promise) {
$true(
strpos($lower, $promise) === false,
'copy promises "' . $promise . '" — the feed is advisory on every plan and '
. 'writes nothing to the block list (#476): ' . $text
);
}
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions admin/css/webdecoy-admin.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1022,18 +1022,21 @@
white-space: nowrap;
}

.webdecoy-digest-status {
.webdecoy-digest-status,
.webdecoy-alerts-status {
display: flex;
align-items: center;
gap: 6px;
color: #3c4858;
margin: 16px 0 0;
}

.webdecoy-digest-status .dashicons {
.webdecoy-digest-status .dashicons,
.webdecoy-alerts-status .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
flex-shrink: 0;
}

.webdecoy-connected-actions {
Expand Down
26 changes: 25 additions & 1 deletion admin/partials/settings-page.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -804,6 +804,7 @@
$wd_plan_slug = isset($options['plan']) ? (string) $options['plan'] : '';
$wd_plan_label = WebDecoy_Cloud_Connect::plan_label($wd_plan_slug);
$wd_digest_on = !empty($wd_entitlements['digest']['enabled']);
$wd_alerts_on = !empty($wd_entitlements['features']['alerts']);
?>
<div class="webdecoy-connected-card">
<div class="webdecoy-connected-head">
Expand All@@ -826,6 +827,25 @@
?>
</p>

<?php
// Real-time alerts are configured in the dashboard, not here: the
// plugin sends nothing itself, which keeps the entitlement gating a
// cloud response rather than local behaviour. This row exists so a
// paying site can find the feature, and an unpaid one can see what
// it would get, without either having to guess.
?>
<p class="webdecoy-alerts-status">
<span class="dashicons <?php echo esc_attr($wd_alerts_on ? 'dashicons-bell' : 'dashicons-lock'); ?>"></span>
<?php if ($wd_alerts_on) : ?>
<?php esc_html_e('Slack and webhook alerts: On.', 'webdecoy'); ?>
<a href="https://app.webdecoy.com/enforcement/actions" target="_blank" rel="noopener">
<?php esc_html_e('Configure them in your dashboard', 'webdecoy'); ?>
</a>
<?php else : ?>
<?php esc_html_e('Slack and webhook alerts: available on Pro. Detection, blocking and the monthly report stay free.', 'webdecoy'); ?>
<?php endif; ?>
</p>

<p class="webdecoy-connected-actions">
<a href="<?php echo esc_url(WebDecoy_Cloud_Connect::wp_upgrade_url('wp_pro')); ?>" class="button button-primary" target="_blank" rel="noopener">
<?php esc_html_e('Upgrade', 'webdecoy'); ?>
Expand DownExpand Up@@ -856,7 +876,11 @@
<li><?php esc_html_e('VPN, proxy, and Tor exit node detection', 'webdecoy'); ?></li>
<li><?php esc_html_e('Cross-site threat intelligence from all WebDecoy users', 'webdecoy'); ?></li>
<li><?php esc_html_e('Advanced cloud analytics with indefinite data retention', 'webdecoy'); ?></li>
<li><?php esc_html_e('Webhook and email alert automation', 'webdecoy'); ?></li>
<?php // Not "email alerts": per-detection email is refused by the API
// (#702) because decoys are public bait and a busy site records
// thousands of hits a day. Email is the digest; webhooks are the
// real-time channel, configured in the dashboard. ?>
<li><?php esc_html_e('Webhook automation and a monthly email report', 'webdecoy'); ?></li>
</ul>
<p>
<a href="https://webdecoy.com/pricing" class="webdecoy-text-link" target="_blank" rel="noopener">
Expand Down
7 changes: 6 additions & 1 deletion includes/class-webdecoy-critical-moment.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,7 +217,12 @@ private function build_notice(string $ip): array
case 'unconnected':
default:
return [
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network — and to block threats like it automatically.', 'webdecoy'),
// Was "and to block threats like it automatically". Connecting does
// not block anything: the cross-site feed is advisory on every plan
// and writes nothing to the block list (#476). The unconnected pitch
// was the last place in this file still promising it, three variants
// below a comment forbidding exactly that claim.
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network, and what it has been doing to other sites.', 'webdecoy'),
'cta_label' => __('Connect to WebDecoy Cloud', 'webdecoy'),
'cta_url' => admin_url('admin.php?page=webdecoy&tab=cloud'),
'type' => 'warning',
Expand Down
8 changes: 4 additions & 4 deletions readme.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,16 +148,16 @@ And because **monitor mode is the default**, baking WebDecoy into your boilerpla

Connect an API key to unlock cloud-powered intelligence:

* **WAF Integrations**: arm your existing edge. Push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **WAF Integrations**: arm your existing edge. From your WebDecoy dashboard, push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **IP Reputation**: AbuseIPDB integration, threat scoring
* **VPN/Proxy Detection**: identify visitors hiding behind VPNs, proxies, and Tor
* **GeoIP Enrichment**: geographic data from MaxMind
* **Cloud Sync**: forward detections to a centralized dashboard
* **Cross-Site Intelligence**: aggregate threat data from all WebDecoy customers
* **Advanced Analytics**: cloud dashboard at app.webdecoy.com with indefinite history
* **Webhooks & Alerts**: automated response chains, email notifications
* **Webhooks & Alerts**: automated response chains, plus a monthly email report of what was caught

[Explore Plans](https://webdecoy.com/pricing) | [Start Free Trial](https://app.webdecoy.com/register)
[Explore Plans](https://webdecoy.com/pricing) | [Create a free account](https://app.webdecoy.com/register)

= Threat Scoring =

Expand DownExpand Up@@ -212,7 +212,7 @@ Firewalls match requests against known-bad signatures, and scanners look for inf

= What does WebDecoy Cloud add? =

WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, automated response features like webhooks and email alerts, and WAF integrations: confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).
WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, a monthly email report, webhook automation, and WAF integrations: from your dashboard, confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).

= Does WebDecoy slow down my site? =

Expand Down
40 changes: 40 additions & 0 deletions tests/CriticalMomentTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,43 @@
$same('connected_upgrade', WebDecoy_Critical_Moment::variant(true, true, false), 'known + no feed -> upgrade pitch');
$same('connected_covered', WebDecoy_Critical_Moment::variant(true, true, true), 'known + feed -> confirmation copy');
});

echo "\nCritical Moment: the copy must not promise a block\n";

/**
* The four message variants are built inside a method that calls WordPress, so
* they cannot be invoked here. Their text can still be read.
*
* That is worth doing because this exact claim has now been removed from this
* file twice. #476 measured the cross-site feed and withdrew blocking from it:
* 0.04% of addresses were ever seen at a second site, 82% of feed entries were
* already a week stale, and none were still active. The connected variants were
* corrected then, with a comment above them saying not to claim it. The
* unconnected variant kept promising "to block threats like it automatically"
* for another three weeks, four lines below that comment.
*
* A comment asking the next person not to do something is not a guard. This is.
*/
$t('no translatable string in the file offers to block anything', function () use ($true) {
$src = file_get_contents(dirname(__DIR__) . '/includes/class-webdecoy-critical-moment.php');
$true($src !== false, 'source is readable');

// Only the translated strings: comments in this file discuss blocking at
// length, and must be free to keep doing so.
preg_match_all("/(?:__|_e|esc_html__|esc_html_e)\(\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $singles);
preg_match_all("/_n\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $plurals);

$strings = array_merge($singles[1], $plurals[1], $plurals[2]);
$true(count($strings) >= 4, 'found the message strings (' . count($strings) . ')');

foreach ($strings as $text) {
$lower = strtolower($text);
foreach (['block', 'prevent', 'stop them'] as $promise) {
$true(
strpos($lower, $promise) === false,
'copy promises "' . $promise . '" — the feed is advisory on every plan and '
. 'writes nothing to the block list (#476): ' . $text
);
}
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions admin/css/webdecoy-admin.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1022,18 +1022,21 @@
white-space: nowrap;
}

.webdecoy-digest-status {
.webdecoy-digest-status,
.webdecoy-alerts-status {
display: flex;
align-items: center;
gap: 6px;
color: #3c4858;
margin: 16px 0 0;
}

.webdecoy-digest-status .dashicons {
.webdecoy-digest-status .dashicons,
.webdecoy-alerts-status .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
flex-shrink: 0;
}

.webdecoy-connected-actions {
Expand Down
26 changes: 25 additions & 1 deletion admin/partials/settings-page.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -804,6 +804,7 @@
$wd_plan_slug = isset($options['plan']) ? (string) $options['plan'] : '';
$wd_plan_label = WebDecoy_Cloud_Connect::plan_label($wd_plan_slug);
$wd_digest_on = !empty($wd_entitlements['digest']['enabled']);
$wd_alerts_on = !empty($wd_entitlements['features']['alerts']);
?>
<div class="webdecoy-connected-card">
<div class="webdecoy-connected-head">
Expand All@@ -826,6 +827,25 @@
?>
</p>

<?php
// Real-time alerts are configured in the dashboard, not here: the
// plugin sends nothing itself, which keeps the entitlement gating a
// cloud response rather than local behaviour. This row exists so a
// paying site can find the feature, and an unpaid one can see what
// it would get, without either having to guess.
?>
<p class="webdecoy-alerts-status">
<span class="dashicons <?php echo esc_attr($wd_alerts_on ? 'dashicons-bell' : 'dashicons-lock'); ?>"></span>
<?php if ($wd_alerts_on) : ?>
<?php esc_html_e('Slack and webhook alerts: On.', 'webdecoy'); ?>
<a href="https://app.webdecoy.com/enforcement/actions" target="_blank" rel="noopener">
<?php esc_html_e('Configure them in your dashboard', 'webdecoy'); ?>
</a>
<?php else : ?>
<?php esc_html_e('Slack and webhook alerts: available on Pro. Detection, blocking and the monthly report stay free.', 'webdecoy'); ?>
<?php endif; ?>
</p>

<p class="webdecoy-connected-actions">
<a href="<?php echo esc_url(WebDecoy_Cloud_Connect::wp_upgrade_url('wp_pro')); ?>" class="button button-primary" target="_blank" rel="noopener">
<?php esc_html_e('Upgrade', 'webdecoy'); ?>
Expand DownExpand Up@@ -856,7 +876,11 @@
<li><?php esc_html_e('VPN, proxy, and Tor exit node detection', 'webdecoy'); ?></li>
<li><?php esc_html_e('Cross-site threat intelligence from all WebDecoy users', 'webdecoy'); ?></li>
<li><?php esc_html_e('Advanced cloud analytics with indefinite data retention', 'webdecoy'); ?></li>
<li><?php esc_html_e('Webhook and email alert automation', 'webdecoy'); ?></li>
<?php // Not "email alerts": per-detection email is refused by the API
// (#702) because decoys are public bait and a busy site records
// thousands of hits a day. Email is the digest; webhooks are the
// real-time channel, configured in the dashboard. ?>
<li><?php esc_html_e('Webhook automation and a monthly email report', 'webdecoy'); ?></li>
</ul>
<p>
<a href="https://webdecoy.com/pricing" class="webdecoy-text-link" target="_blank" rel="noopener">
Expand Down
7 changes: 6 additions & 1 deletion includes/class-webdecoy-critical-moment.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,7 +217,12 @@ private function build_notice(string $ip): array
case 'unconnected':
default:
return [
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network — and to block threats like it automatically.', 'webdecoy'),
// Was "and to block threats like it automatically". Connecting does
// not block anything: the cross-site feed is advisory on every plan
// and writes nothing to the block list (#476). The unconnected pitch
// was the last place in this file still promising it, three variants
// below a comment forbidding exactly that claim.
'message' => __('A CRITICAL deception trap was just tripped. Connect to WebDecoy Cloud to see whether this attacker is already known across the network, and what it has been doing to other sites.', 'webdecoy'),
'cta_label' => __('Connect to WebDecoy Cloud', 'webdecoy'),
'cta_url' => admin_url('admin.php?page=webdecoy&tab=cloud'),
'type' => 'warning',
Expand Down
8 changes: 4 additions & 4 deletions readme.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,16 +148,16 @@ And because **monitor mode is the default**, baking WebDecoy into your boilerpla

Connect an API key to unlock cloud-powered intelligence:

* **WAF Integrations**: arm your existing edge. Push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **WAF Integrations**: arm your existing edge. From your WebDecoy dashboard, push confirmed attackers to Cloudflare or AWS WAF so they're blocked before a request ever reaches WordPress, plus webhooks for any other firewall
* **IP Reputation**: AbuseIPDB integration, threat scoring
* **VPN/Proxy Detection**: identify visitors hiding behind VPNs, proxies, and Tor
* **GeoIP Enrichment**: geographic data from MaxMind
* **Cloud Sync**: forward detections to a centralized dashboard
* **Cross-Site Intelligence**: aggregate threat data from all WebDecoy customers
* **Advanced Analytics**: cloud dashboard at app.webdecoy.com with indefinite history
* **Webhooks & Alerts**: automated response chains, email notifications
* **Webhooks & Alerts**: automated response chains, plus a monthly email report of what was caught

[Explore Plans](https://webdecoy.com/pricing) | [Start Free Trial](https://app.webdecoy.com/register)
[Explore Plans](https://webdecoy.com/pricing) | [Create a free account](https://app.webdecoy.com/register)

= Threat Scoring =

Expand DownExpand Up@@ -212,7 +212,7 @@ Firewalls match requests against known-bad signatures, and scanners look for inf

= What does WebDecoy Cloud add? =

WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, automated response features like webhooks and email alerts, and WAF integrations: confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).
WebDecoy Cloud adds threat intelligence feeds (AbuseIPDB, VPNAPI, MaxMind), centralized monitoring across multiple sites, indefinite detection history, a monthly email report, webhook automation, and WAF integrations: from your dashboard, confirmed attackers can be pushed to Cloudflare or AWS WAF so they're blocked at the edge, before ever reaching your server. [Compare plans](https://webdecoy.com/pricing).

= Does WebDecoy slow down my site? =

Expand Down
40 changes: 40 additions & 0 deletions tests/CriticalMomentTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,3 +56,43 @@
$same('connected_upgrade', WebDecoy_Critical_Moment::variant(true, true, false), 'known + no feed -> upgrade pitch');
$same('connected_covered', WebDecoy_Critical_Moment::variant(true, true, true), 'known + feed -> confirmation copy');
});

echo "\nCritical Moment: the copy must not promise a block\n";

/**
* The four message variants are built inside a method that calls WordPress, so
* they cannot be invoked here. Their text can still be read.
*
* That is worth doing because this exact claim has now been removed from this
* file twice. #476 measured the cross-site feed and withdrew blocking from it:
* 0.04% of addresses were ever seen at a second site, 82% of feed entries were
* already a week stale, and none were still active. The connected variants were
* corrected then, with a comment above them saying not to claim it. The
* unconnected variant kept promising "to block threats like it automatically"
* for another three weeks, four lines below that comment.
*
* A comment asking the next person not to do something is not a guard. This is.
*/
$t('no translatable string in the file offers to block anything', function () use ($true) {
$src = file_get_contents(dirname(__DIR__) . '/includes/class-webdecoy-critical-moment.php');
$true($src !== false, 'source is readable');

// Only the translated strings: comments in this file discuss blocking at
// length, and must be free to keep doing so.
preg_match_all("/(?:__|_e|esc_html__|esc_html_e)\(\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $singles);
preg_match_all("/_n\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/", $src, $plurals);

$strings = array_merge($singles[1], $plurals[1], $plurals[2]);
$true(count($strings) >= 4, 'found the message strings (' . count($strings) . ')');

foreach ($strings as $text) {
$lower = strtolower($text);
foreach (['block', 'prevent', 'stop them'] as $promise) {
$true(
strpos($lower, $promise) === false,
'copy promises "' . $promise . '" — the feed is advisory on every plan and '
. 'writes nothing to the block list (#476): ' . $text
);
}
}
});
Loading