diff --git a/admin/partials/settings-page.php b/admin/partials/settings-page.php
index fe67bc1..04c6a5d 100644
--- a/admin/partials/settings-page.php
+++ b/admin/partials/settings-page.php
@@ -636,6 +636,17 @@
+
|
diff --git a/changelog.txt b/changelog.txt
index b289a61..4452094 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -9,6 +9,7 @@
* Added: Rule engine — deterministic rules evaluated before heuristic scoring; first DENY/THROTTLE wins, with dry-run (log without blocking). Parity with @webdecoy/node.
* Added: Tripwires (deception layer) — deterministic, zero-false-positive blocking of hidden honeypot paths (scanner-bait like /.env, /.git/config, /wp-config.php). On by default. Custom exact paths, prefixes, and regex patterns; block or throttle; dry-run. New Settings → Tripwires tab.
* Added: Honeytoken — automatically injects an invisible decoy link on front-end pages pointing at a secret per-site path; only link-following scrapers ever request it, and a hit is armed as a tripwire. On by default, with optional daily rotation. Never shown to real visitors or logged-in users.
+* Added: WooCommerce honeytoken coupons — a hidden decoy promo code is planted on cart/checkout pages where coupon-scraping bots look but no human ever sees it. Applying it (classic checkout or Blocks/Store API) is a deterministic bot signal: recorded and, per your blocking setting, blocked. Optional daily rotation with a grace window; real coupons and shoppers are never affected.
* Added: WordPress-native traps — fake vulnerable-plugin paths (armed only for plugins not actually installed, so real plugins are never shadowed), optional XML-RPC probing trap (off by default), and author-enumeration protection: ?author=N and REST user enumeration return a canary username instead of leaking real ones, and a later login attempt with that canary is flagged as a critical exfiltration detection.
* Added: Deceptive tripwire responses — a tripwire hit can serve a 404, believable fake content (fake .env / wp-config / SQL dump / phpinfo seeded with unique per-site canary credentials), or a slow-drip tarpit, instead of a plain 403. A later login attempt using a canary credential is logged as a critical exfiltration detection and blocked. Decoy content is template-only and never exposes real configuration.
* Added: wd_clearance forwarding — a tripwire hit carrying the visitor's wd_clearance cookie is reported so the WebDecoy Cloud can durably deny the actor's device fingerprint (rotation-proof lockout). Heuristic rules never carry the token.
diff --git a/includes/class-webdecoy-woocommerce.php b/includes/class-webdecoy-woocommerce.php
index 8b62d41..81b3686 100644
--- a/includes/class-webdecoy-woocommerce.php
+++ b/includes/class-webdecoy-woocommerce.php
@@ -518,8 +518,109 @@ public function get_suspicious_ips(int $threshold = 3): array
$threshold
), ARRAY_A) ?: [];
}
+
+ /**
+ * The honeytoken coupon code for this site — a fake promo code planted where
+ * coupon-scraping bots look but no human ever sees. Deterministic from a
+ * per-site secret; optionally rotates daily. Applying it is, by
+ * construction, an automated action.
+ */
+ public function honeytoken_coupon(bool $rotate = false): string
+ {
+ $secret = get_option('webdecoy_coupon_secret', '');
+ if (!is_string($secret) || $secret === '') {
+ $secret = bin2hex(random_bytes(16));
+ add_option('webdecoy_coupon_secret', $secret, '', 'yes');
+ }
+ $label = $rotate ? ('day:' . gmdate('Y-m-d')) : 'stable';
+ return 'WD' . strtoupper(substr(hash_hmac('sha256', $label, $secret), 0, 8));
+ }
+
+ /**
+ * Hidden markup exposing the honeytoken coupon to page scrapers only. Placed
+ * in an HTML comment plus an offscreen, aria-hidden node so it never appears
+ * to a human or in the accessibility tree.
+ */
+ public function render_coupon_bait(): void
+ {
+ if (empty($this->options['woo_honeytoken_coupons'])) {
+ return;
+ }
+ $code = $this->honeytoken_coupon(!empty($this->options['honeytoken_rotate']));
+ echo "\n\n";
+ echo ' '
+ . '' . esc_html($code) . ' ' . "\n";
+ }
+
+ /**
+ * Coupon-load filter: if the code being applied is our honeytoken, record a
+ * detection (and optionally block), then report it invalid. Runs for both
+ * classic checkout and the Store API / Blocks, since both load coupons via
+ * WC_Coupon → this filter. Real coupons are untouched.
+ *
+ * @param mixed $data Coupon data (false when no matching coupon post).
+ * @param string $code The coupon code being looked up.
+ * @return mixed
+ */
+ public function catch_coupon($data, string $code)
+ {
+ if (empty($this->options['woo_honeytoken_coupons'])) {
+ return $data;
+ }
+
+ $canary = $this->honeytoken_coupon(!empty($this->options['honeytoken_rotate']));
+ // Also honor the previous day's code during rotation grace.
+ $prev = null;
+ if (!empty($this->options['honeytoken_rotate'])) {
+ $secret = get_option('webdecoy_coupon_secret', '');
+ if (is_string($secret) && $secret !== '') {
+ $prev = 'WD' . strtoupper(substr(hash_hmac('sha256', 'day:' . gmdate('Y-m-d', time() - DAY_IN_SECONDS), $secret), 0, 8));
+ }
+ }
+
+ $normalized = strtoupper(trim($code));
+ if ($normalized === $canary || ($prev !== null && $normalized === $prev)) {
+ $ip = $this->get_client_ip();
+ $this->log_detection($ip, 'honeytoken_coupon', 100);
+
+ if (($this->options['block_action'] ?? 'block') === 'block') {
+ $this->blocker->block(
+ $ip,
+ 'Honeytoken coupon applied',
+ ($this->options['block_duration'] ?? 24) > 0 ? $this->options['block_duration'] : null
+ );
+ }
+
+ return false; // reject as a non-existent coupon
+ }
+
+ return $data;
+ }
}
+// Honeytoken coupons: plant a hidden fake code and catch anyone who applies it.
+add_action('woocommerce_before_cart', function () {
+ $options = get_option('webdecoy_options', []);
+ if (empty($options['protect_checkout']) || empty($options['woo_honeytoken_coupons'])) {
+ return;
+ }
+ (new WebDecoy_WooCommerce($options))->render_coupon_bait();
+});
+add_action('woocommerce_before_checkout_form', function () {
+ $options = get_option('webdecoy_options', []);
+ if (empty($options['protect_checkout']) || empty($options['woo_honeytoken_coupons'])) {
+ return;
+ }
+ (new WebDecoy_WooCommerce($options))->render_coupon_bait();
+});
+add_filter('woocommerce_get_shop_coupon_data', function ($data, $code) {
+ $options = get_option('webdecoy_options', []);
+ if (empty($options['protect_checkout']) || empty($options['woo_honeytoken_coupons'])) {
+ return $data;
+ }
+ return (new WebDecoy_WooCommerce($options))->catch_coupon($data, (string) $code);
+}, 10, 2);
+
// Hook into WooCommerce payment completion - track success
add_action('woocommerce_payment_complete', function ($order_id) {
$options = get_option('webdecoy_options', []);
diff --git a/webdecoy.php b/webdecoy.php
index c9ff333..ff63430 100644
--- a/webdecoy.php
+++ b/webdecoy.php
@@ -255,6 +255,9 @@ private function load_options(): void
'protect_checkout' => true,
'checkout_velocity_limit' => 5,
'checkout_velocity_window' => 3600,
+ // Plant a hidden honeytoken coupon; applying it is a deterministic
+ // bot signal (no human ever sees the code).
+ 'woo_honeytoken_coupons' => true,
// Client-side Scanner
'scanner_enabled' => true,
@@ -1834,6 +1837,7 @@ public function sanitize_options(array $input): array
$sanitized['protect_checkout'] = !empty($input['protect_checkout']);
$sanitized['checkout_velocity_limit'] = max(1, intval($input['checkout_velocity_limit'] ?? 5));
$sanitized['checkout_velocity_window'] = max(60, intval($input['checkout_velocity_window'] ?? 3600));
+ $sanitized['woo_honeytoken_coupons'] = !empty($input['woo_honeytoken_coupons']);
// Proof-of-Work
$sanitized['pow_enabled'] = !empty($input['pow_enabled']);
|