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
395 changes: 379 additions & 16 deletions README.md

Large diffs are not rendered by default.

566 changes: 457 additions & 109 deletions admin/class-baskerville-admin.php

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions assets/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,9 @@ h3.baskerville-section-title {
.baskerville-ml-10 {
margin-left: 10px;
}
.baskerville-ml-20 {
margin-left: 20px;
}
.baskerville-my-20 {
margin: 20px 0;
}
Expand Down Expand Up @@ -960,10 +963,16 @@ h3.baskerville-section-title {
border: 2px solid var(--bsk-color-warning-yellow);
background: var(--bsk-color-warning-bg);
}
/* Under Attack Mode active — override border/bg */
.baskerville-master-switch-attack {
border-color: var(--bsk-color-danger) !important;
background: var(--bsk-color-danger-bg) !important;
}
.baskerville-master-switch-header {
display: flex;
align-items: center;
gap: 30px;
flex-wrap: wrap;
}
.baskerville-master-switch-title {
margin: 0;
Expand All @@ -975,6 +984,104 @@ h3.baskerville-section-title {
color: var(--bsk-color-warning-dark);
}

/* Under Attack quick-toggle in master switch bar */
.baskerville-under-attack-quick {
margin-left: auto;
padding-left: 20px;
border-left: 2px solid rgba(0,0,0,.08);
}

/* Red toggle slider — used for Under Attack Mode */
.baskerville-toggle-slider-danger {
background-color: var(--bsk-color-danger) !important;
}
/* Under Attack slider turns red immediately on check (no JS needed) */
.baskerville-under-attack-quick input:checked + .baskerville-toggle-slider {
background-color: var(--bsk-color-danger);
}

/* Clear All Bans quick button */
.baskerville-clear-bans-quick {
display: flex;
align-items: center;
gap: 10px;
}
.baskerville-btn-clear-bans {
display: inline-flex;
align-items: center;
gap: 4px;
border-color: var(--bsk-color-danger-alt) !important;
color: var(--bsk-color-danger-alt) !important;
}
.baskerville-btn-clear-bans:hover {
background: var(--bsk-color-danger-bg) !important;
}
.baskerville-clear-bans-msg {
font-size: 13px;
font-weight: 500;
}

/* Disabled toggle label — grayed out, no pointer */
.baskerville-toggle-disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}

/* No-challenge warning banner */
.baskerville-no-challenge-banner {
display: flex;
align-items: flex-start;
gap: 20px;
margin: 0 0 24px 0;
padding: 24px 28px;
border-radius: 8px;
border: 3px solid var(--bsk-color-danger);
background: #fff;
}
.baskerville-no-challenge-banner-icon .dashicons {
font-size: 48px;
width: 48px;
height: 48px;
color: var(--bsk-color-danger);
flex-shrink: 0;
margin-top: 4px;
}
.baskerville-no-challenge-banner-body {
display: flex;
flex-direction: column;
gap: 8px;
}
.baskerville-no-challenge-banner-title {
font-size: 22px;
font-weight: 700;
color: var(--bsk-color-danger);
line-height: 1.2;
}
.baskerville-no-challenge-banner-text {
font-size: 15px;
color: #333;
line-height: 1.6;
max-width: 680px;
}
.baskerville-no-challenge-banner-cta {
display: inline-block;
margin-top: 6px;
padding: 10px 22px;
border-radius: 5px;
background: var(--bsk-color-danger);
color: #fff !important;
font-size: 15px;
font-weight: 600;
text-decoration: none !important;
transition: background .15s;
align-self: flex-start;
}
.baskerville-no-challenge-banner-cta:hover {
background: var(--bsk-color-danger-dark) !important;
color: #fff !important;
}

/* Simple table for diagnostics */
.baskerville-simple-table {
margin: 10px 0;
Expand Down
11 changes: 7 additions & 4 deletions assets/js/live-feed.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ jQuery(document).ready(function($) {
var icon = getEventIcon(event.classification, event.event_type);
var color = getEventColor(event.classification, event.event_type);
var timeAgo = getTimeAgo(event.created_at);
var isTurnstileFail = event.event_type === 'ts_fail';
var displayLabel = isTurnstileFail ? i18n.turnstileFailed : event.classification.toUpperCase().replace('_', ' ');
var isTurnstileFail = event.event_type === 'ts_fail' || event.event_type === 'gk_fail';
var displayLabel = isTurnstileFail ? i18n.challengeFailed : event.classification.toUpperCase().replace('_', ' ');

var banBadge = '';
if (isTurnstileFail) {
Expand Down Expand Up @@ -121,6 +121,9 @@ jQuery(document).ready(function($) {
var item = $('<div class="live-feed-item"></div>');
var countryName = event.country_code ? getCountryName(event.country_code) : '';
var reasonText = isTurnstileFail ? i18n.failedTurnstile : (event.reason || i18n.noReason);
if (event.block_reason === 'gk-challenge-fail') {
reasonText = i18n.failedTurnstile;
}
item.html(
'<span class="feed-icon">' + icon + '</span> ' +
'<strong style="color: ' + color + ';">' + displayLabel + '</strong>' +
Expand Down Expand Up @@ -163,7 +166,7 @@ jQuery(document).ready(function($) {
}

function getEventIcon(classification, eventType) {
if (eventType === 'ts_fail') return '\u{1f6e1}\ufe0f';
if (eventType === 'ts_fail' || eventType === 'gk_fail') return '\u{1f6e1}\ufe0f';
if (eventType === 'honeypot') return '\u{1f36f}';
if (classification === 'ai_bot') return '\u{1f916}';
if (classification === 'bad_bot') return '\u{1f534}';
Expand All @@ -172,7 +175,7 @@ jQuery(document).ready(function($) {
}

function getEventColor(classification, eventType) {
if (eventType === 'ts_fail') return '#dc2626';
if (eventType === 'ts_fail' || eventType === 'gk_fail') return '#dc2626';
if (classification === 'ai_bot') return '#9333ea';
if (classification === 'bad_bot') return '#dc2626';
if (classification === 'bot') return '#f59e0b';
Expand Down
27 changes: 21 additions & 6 deletions baskerville-ai-security.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
require_once BASKERVILLE_PLUGIN_PATH . 'includes/class-baskerville-honeypot.php';
require_once BASKERVILLE_PLUGIN_PATH . 'includes/class-baskerville-installer.php';
require_once BASKERVILLE_PLUGIN_PATH . 'includes/class-baskerville-maxmind-installer.php';
require_once BASKERVILLE_PLUGIN_PATH . 'includes/class-baskerville-turnstile.php';
require_once BASKERVILLE_PLUGIN_PATH . 'admin/class-baskerville-admin.php';

// Add custom cron intervals
Expand All @@ -52,9 +51,23 @@
$aiua = new Baskerville_AI_UA($core); // AI_UA should receive $core in constructor
$stats = new Baskerville_Stats($core, $aiua); // Stats receives Core and AI_UA

// Cloudflare Turnstile - must be created BEFORE firewall for borderline challenge
$turnstile = new Baskerville_Turnstile($core, $stats);
$GLOBALS['baskerville_turnstile'] = $turnstile;
// Challenge provider — must be created BEFORE firewall for borderline challenge decisions
$options_early = get_option('baskerville_settings', array());
$captcha_provider = isset($options_early['captcha_provider']) ? $options_early['captcha_provider'] : 'none';

if ($captcha_provider === 'gatekeeper') {
require_once BASKERVILLE_PLUGIN_PATH . 'includes/class-baskerville-gatekeeper.php';
$challenge_obj = new Baskerville_Gatekeeper($core, $stats);
} elseif ($captcha_provider === 'turnstile') {
require_once BASKERVILLE_PLUGIN_PATH . 'includes/class-baskerville-turnstile.php';
$challenge_obj = new Baskerville_Turnstile($core, $stats);
} else {
$challenge_obj = null;
}

if ($challenge_obj !== null) {
$GLOBALS['baskerville_challenge'] = $challenge_obj;
}

// pre-DB firewall (MUST run IMMEDIATELY, before any other hooks)
// This runs directly in plugins_loaded to catch requests as early as possible
Expand All @@ -79,8 +92,10 @@
$honeypot = new Baskerville_Honeypot($core, $stats, $aiua);
$honeypot->init();

// Initialize Turnstile hooks (object already created before firewall)
$turnstile->init();
// Initialize challenge provider hooks (object already created before firewall)
if ($challenge_obj !== null) {
$challenge_obj->init();
}

// periodic statistics cleanup
add_action('baskerville_cleanup_stats', [$stats, 'cleanup_old_stats']);
Expand Down
56 changes: 55 additions & 1 deletion includes/class-baskerville-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ public function enqueue_scripts() {
}

public function enqueue_admin_scripts() {
$css_file = BASKERVILLE_PLUGIN_PATH . 'assets/css/admin.css';
$css_ver = BASKERVILLE_DEBUG ? filemtime($css_file) : BASKERVILLE_VERSION;
wp_enqueue_style(
'baskerville-admin-style',
BASKERVILLE_PLUGIN_URL . 'assets/css/admin.css',
array(),
BASKERVILLE_VERSION
$css_ver
);
}

Expand Down Expand Up @@ -387,6 +389,58 @@ public function fc_clear_geoip_cache() {
return $cleared;
}

/**
* Clear all IP ban entries AND challenge-fail counters from the cache.
* Called by the "Clear All Bans" admin button to give a full clean slate.
* @return int Number of entries cleared
*/
public function fc_clear_bans(): int {
$cleared = 0;

if ($this->fc_has_apcu()) {
// Clear ban entries (ban:{ip})
$iterator = new \APCUIterator('/^baskerville:ban:/');
foreach ($iterator as $entry) {
if (apcu_delete($entry['key'])) {
$cleared++;
}
}
// Also clear challenge-fail counters (gk_fail:{ip}) so the
// threshold resets alongside the ban — prevents leftover counters
// from triggering an instant ban on the next failure.
$iterator = new \APCUIterator('/^baskerville:gk_fail:/');
foreach ($iterator as $entry) {
if (apcu_delete($entry['key'])) {
$cleared++;
}
}
} else {
$dir = $this->fc_dir();
if (!is_dir($dir)) return 0;

$files = @glob($dir . '/*.cache');
if (!$files) return 0;

foreach ($files as $file) {
$raw = @file_get_contents($file);
if ($raw === false) continue;
$data = @unserialize($raw);
if (!is_array($data)) continue;
$v = $data['v'] ?? null;
// Ban entries: array with 'reason' and 'until' keys
$is_ban = is_array($v) && isset($v['reason'], $v['until']);
// Fail counters: plain integers stored by fc_inc_in_window
$is_fail_counter = is_int($v);
if ($is_ban || $is_fail_counter) {
wp_delete_file($file);
$cleared++;
}
}
}

return $cleared;
}

public function fc_has_apcu(): bool {
return function_exists('apcu_store') && (function_exists('apcu_enabled') ? apcu_enabled() : true);
}
Expand Down
Loading