|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +/** |
| 6 | + * Tests for WebDecoy_Honeytoken (F4 hidden-link injection). |
| 7 | + * |
| 8 | + * Self-contained: defines the handful of WordPress functions the class touches |
| 9 | + * (guarded so they don't collide with other tests or a real WP runtime), then |
| 10 | + * exercises token derivation, the hidden-link markup, rotation, and that a |
| 11 | + * honeytoken path actually trips a tripwire. Run: php tests/run.php |
| 12 | + */ |
| 13 | + |
| 14 | +use WebDecoy\Rules\RuleContext; |
| 15 | +use WebDecoy\Rules\RuleEngine; |
| 16 | +use WebDecoy\Rules\RuleResult; |
| 17 | +use WebDecoy\Rules\TripwireRule; |
| 18 | + |
| 19 | +if (!defined('ABSPATH')) { |
| 20 | + define('ABSPATH', '/tmp/'); |
| 21 | +} |
| 22 | +if (!defined('DAY_IN_SECONDS')) { |
| 23 | + define('DAY_IN_SECONDS', 86400); |
| 24 | +} |
| 25 | +if (!isset($GLOBALS['__wd_opts'])) { |
| 26 | + $GLOBALS['__wd_opts'] = []; |
| 27 | +} |
| 28 | +if (!function_exists('get_option')) { |
| 29 | + function get_option($k, $d = false) |
| 30 | + { |
| 31 | + return $GLOBALS['__wd_opts'][$k] ?? $d; |
| 32 | + } |
| 33 | +} |
| 34 | +if (!function_exists('add_option')) { |
| 35 | + function add_option($k, $v, $a = '', $b = 'yes') |
| 36 | + { |
| 37 | + $GLOBALS['__wd_opts'][$k] = $v; |
| 38 | + return true; |
| 39 | + } |
| 40 | +} |
| 41 | +if (!function_exists('esc_attr')) { |
| 42 | + function esc_attr($s) |
| 43 | + { |
| 44 | + return htmlspecialchars((string) $s, ENT_QUOTES); |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +require_once dirname(__DIR__) . '/includes/class-webdecoy-honeytoken.php'; |
| 49 | + |
| 50 | +$t = ['TestRunner', 'test']; |
| 51 | +$eq = ['TestRunner', 'assertSame']; |
| 52 | +$true = ['TestRunner', 'assertTrue']; |
| 53 | + |
| 54 | +echo "\nWebDecoy_Honeytoken\n"; |
| 55 | + |
| 56 | +$t('derives a stable /__wd/{12-hex} path from the per-site secret', function () use ($eq, $true) { |
| 57 | + $h = new WebDecoy_Honeytoken(false); |
| 58 | + $p = $h->primary_path(); |
| 59 | + $true(strpos($p, '/__wd/') === 0, 'under /__wd/'); |
| 60 | + $eq(strlen('/__wd/') + 12, strlen($p), '12-hex token'); |
| 61 | + $eq($p, $h->primary_path(), 'deterministic across calls'); |
| 62 | + $eq($p, (new WebDecoy_Honeytoken(false))->primary_path(), 'stable across instances (same secret)'); |
| 63 | +}); |
| 64 | + |
| 65 | +$t('stable mode arms exactly the advertised path', function () use ($eq) { |
| 66 | + $h = new WebDecoy_Honeytoken(false); |
| 67 | + $paths = $h->active_paths(); |
| 68 | + $eq(1, count($paths)); |
| 69 | + $eq($h->primary_path(), $paths[0]); |
| 70 | +}); |
| 71 | + |
| 72 | +$t('persists an unguessable secret to options', function () use ($true) { |
| 73 | + (new WebDecoy_Honeytoken(false))->primary_path(); |
| 74 | + $secret = $GLOBALS['__wd_opts']['webdecoy_honeytoken_secret'] ?? ''; |
| 75 | + $true(is_string($secret) && strlen($secret) >= 16, 'secret stored'); |
| 76 | +}); |
| 77 | + |
| 78 | +$t('hidden link matches the node hiding technique', function () use ($true) { |
| 79 | + $h = new WebDecoy_Honeytoken(false); |
| 80 | + $link = $h->render_link(); |
| 81 | + $true(strpos($link, 'href="' . $h->primary_path() . '"') !== false, 'href = primary path'); |
| 82 | + $true(strpos($link, 'aria-hidden="true"') !== false, 'aria-hidden'); |
| 83 | + $true(strpos($link, 'tabindex="-1"') !== false, 'tabindex -1'); |
| 84 | + $true(strpos($link, 'rel="nofollow noindex"') !== false, 'nofollow noindex'); |
| 85 | + $true(strpos($link, 'position:absolute;left:-9999px') !== false, 'offscreen'); |
| 86 | +}); |
| 87 | + |
| 88 | +$t('rotation arms today + yesterday and differs from stable', function () use ($eq, $true) { |
| 89 | + $r = new WebDecoy_Honeytoken(true); |
| 90 | + $paths = $r->active_paths(); |
| 91 | + $eq(2, count($paths), 'today + yesterday grace window'); |
| 92 | + $true(in_array($r->primary_path(), $paths, true), 'today is armed'); |
| 93 | + $true($r->primary_path() !== (new WebDecoy_Honeytoken(false))->primary_path(), 'rotating != stable'); |
| 94 | +}); |
| 95 | + |
| 96 | +$t('a honeytoken path trips a tripwire; normal pages pass', function () use ($eq) { |
| 97 | + $h = new WebDecoy_Honeytoken(false); |
| 98 | + $engine = new RuleEngine([new TripwireRule(['paths' => $h->active_paths(), 'includeDefaults' => false])]); |
| 99 | + $hit = $engine->evaluate(new RuleContext('9.9.9.9', $h->primary_path(), 'GET', 'scrapy', [], 1700000000000)); |
| 100 | + $eq(RuleResult::DENY, $hit->action); |
| 101 | + $eq('tripwire', $hit->rule); |
| 102 | + $miss = $engine->evaluate(new RuleContext('9.9.9.9', '/', 'GET', 'human', [], 1700000000000)); |
| 103 | + $eq(RuleResult::ALLOW, $miss->action); |
| 104 | +}); |
0 commit comments