252 lines
8.2 KiB
PHP
252 lines
8.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Livewire\Admin;
|
|
|
|
use App\Models\Setting;
|
|
use App\Services\Agent\AgentClient;
|
|
use Exception;
|
|
use Filament\Actions\Action;
|
|
use Filament\Actions\Concerns\InteractsWithActions;
|
|
use Filament\Actions\Contracts\HasActions;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Concerns\InteractsWithForms;
|
|
use Filament\Forms\Contracts\HasForms;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
use Filament\Tables\Contracts\HasTable;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Support\Arr;
|
|
use Livewire\Attributes\On;
|
|
use Livewire\Component;
|
|
|
|
class WafWhitelistTable extends Component implements HasTable, HasForms, HasActions
|
|
{
|
|
use InteractsWithTable;
|
|
use InteractsWithForms;
|
|
use InteractsWithActions;
|
|
|
|
#[On('waf-whitelist-updated')]
|
|
public function refreshWhitelist(): void
|
|
{
|
|
$this->resetTable();
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.admin.waf-whitelist-table');
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->records(fn () => $this->getWhitelistRules())
|
|
->paginated([10, 25, 50])
|
|
->defaultPaginationPageOption(10)
|
|
->columns([
|
|
TextColumn::make('label')
|
|
->label(__('Name'))
|
|
->wrap(),
|
|
TextColumn::make('match_type')
|
|
->label(__('Match Type'))
|
|
->formatStateUsing(fn (string $state): string => $this->matchTypeLabel($state))
|
|
->toggleable(),
|
|
TextColumn::make('match_value')
|
|
->label(__('Match Value'))
|
|
->wrap()
|
|
->copyable(),
|
|
TextColumn::make('rule_ids')
|
|
->label(__('Rule IDs'))
|
|
->fontFamily('mono')
|
|
->copyable(),
|
|
])
|
|
->headerActions([
|
|
Action::make('add')
|
|
->label(__('Add Custom Rule'))
|
|
->icon('heroicon-o-plus')
|
|
->modalHeading(__('Add Whitelist Rule'))
|
|
->form($this->whitelistRuleForm())
|
|
->action(function (array $data): void {
|
|
$rules = $this->getWhitelistRules();
|
|
$rules[] = $this->normalizeRule($data);
|
|
$this->persistWhitelistRules($rules);
|
|
|
|
Notification::make()
|
|
->title(__('Whitelist rule added'))
|
|
->success()
|
|
->send();
|
|
}),
|
|
])
|
|
->actions([
|
|
Action::make('delete')
|
|
->label(__('Delete'))
|
|
->icon('heroicon-o-trash')
|
|
->color('danger')
|
|
->requiresConfirmation()
|
|
->action(function (array $record): void {
|
|
$rules = array_values(array_filter($this->getWhitelistRules(), function (array $rule) use ($record) {
|
|
return ($rule['__key'] ?? '') !== ($record['__key'] ?? '');
|
|
}));
|
|
|
|
$this->persistWhitelistRules($rules);
|
|
|
|
Notification::make()
|
|
->title(__('Whitelist rule removed'))
|
|
->success()
|
|
->send();
|
|
}),
|
|
])
|
|
->emptyStateHeading(__('No whitelist rules'))
|
|
->emptyStateDescription(__('Add a custom whitelist rule to allow trusted traffic.'));
|
|
}
|
|
|
|
protected function whitelistRuleForm(): array
|
|
{
|
|
return [
|
|
TextInput::make('label')
|
|
->label(__('Name'))
|
|
->placeholder(__('Rule 942100'))
|
|
->maxLength(80),
|
|
Select::make('match_type')
|
|
->label(__('Match Type'))
|
|
->options([
|
|
'ip' => __('IP Address or CIDR'),
|
|
'uri_exact' => __('Exact URI'),
|
|
'uri_prefix' => __('URI Prefix'),
|
|
'host' => __('Host Header'),
|
|
])
|
|
->required(),
|
|
TextInput::make('match_value')
|
|
->label(__('Match Value'))
|
|
->placeholder(__('Example: 203.0.113.10 or /wp-admin/admin-ajax.php'))
|
|
->required(),
|
|
TextInput::make('rule_ids')
|
|
->label(__('Rule IDs'))
|
|
->placeholder(__('Example: 942100,949110'))
|
|
->helperText(__('Comma-separated ModSecurity rule IDs to disable for matches.'))
|
|
->required(),
|
|
];
|
|
}
|
|
|
|
protected function getWhitelistRules(): array
|
|
{
|
|
$raw = Setting::get('waf_whitelist_rules', '[]');
|
|
$rules = json_decode($raw, true);
|
|
if (! is_array($rules)) {
|
|
return [];
|
|
}
|
|
|
|
$changed = false;
|
|
|
|
foreach ($rules as &$rule) {
|
|
if (! is_array($rule)) {
|
|
$rule = [];
|
|
$changed = true;
|
|
continue;
|
|
}
|
|
|
|
if (array_key_exists('enabled', $rule)) {
|
|
unset($rule['enabled']);
|
|
$changed = true;
|
|
}
|
|
|
|
$rule['label'] = $this->normalizeLabel($rule);
|
|
$rule['__key'] = $this->ruleKey($rule);
|
|
}
|
|
|
|
$rules = array_values(array_filter($rules, fn ($rule) => is_array($rule) && ($rule !== [])));
|
|
|
|
if ($changed) {
|
|
Setting::set('waf_whitelist_rules', json_encode($this->stripIds($rules), JSON_UNESCAPED_SLASHES));
|
|
}
|
|
|
|
return $rules;
|
|
}
|
|
|
|
protected function normalizeRule(array $data): array
|
|
{
|
|
$rule = [
|
|
'label' => $data['label'] ?? null,
|
|
'match_type' => $data['match_type'] ?? '',
|
|
'match_value' => $data['match_value'] ?? '',
|
|
'rule_ids' => $data['rule_ids'] ?? '',
|
|
];
|
|
|
|
$rule['label'] = $this->normalizeLabel($rule);
|
|
$rule['__key'] = $this->ruleKey($rule);
|
|
|
|
return $rule;
|
|
}
|
|
|
|
protected function normalizeLabel(array $rule): string
|
|
{
|
|
$label = trim((string) ($rule['label'] ?? ''));
|
|
if ($label !== '' && !str_contains($label, '{rule}') && !str_contains($label, ':rule')) {
|
|
return $label;
|
|
}
|
|
|
|
$ids = trim((string) ($rule['rule_ids'] ?? ''));
|
|
if ($ids !== '') {
|
|
return __('Rule :id', ['id' => $ids]);
|
|
}
|
|
|
|
$matchValue = trim((string) ($rule['match_value'] ?? ''));
|
|
if ($matchValue !== '') {
|
|
return $matchValue;
|
|
}
|
|
|
|
return __('Whitelist rule');
|
|
}
|
|
|
|
protected function ruleKey(array $rule): string
|
|
{
|
|
return md5(json_encode(Arr::only($rule, ['label', 'match_type', 'match_value', 'rule_ids'])));
|
|
}
|
|
|
|
protected function stripIds(array $rules): array
|
|
{
|
|
return array_map(function (array $rule): array {
|
|
return Arr::only($rule, ['label', 'match_type', 'match_value', 'rule_ids']);
|
|
}, $rules);
|
|
}
|
|
|
|
protected function persistWhitelistRules(array $rules): void
|
|
{
|
|
$rules = $this->stripIds($rules);
|
|
Setting::set('waf_whitelist_rules', json_encode(array_values($rules), JSON_UNESCAPED_SLASHES));
|
|
|
|
try {
|
|
$agent = new AgentClient;
|
|
$agent->wafApplySettings(
|
|
Setting::get('waf_enabled', '0') === '1',
|
|
(string) Setting::get('waf_paranoia', '1'),
|
|
Setting::get('waf_audit_log', '1') === '1',
|
|
$rules
|
|
);
|
|
} catch (Exception $e) {
|
|
Notification::make()
|
|
->title(__('Whitelist saved, but apply failed'))
|
|
->body($e->getMessage())
|
|
->warning()
|
|
->send();
|
|
}
|
|
|
|
$this->dispatch('waf-whitelist-updated');
|
|
$this->dispatch('waf-blocked-updated');
|
|
}
|
|
|
|
protected function matchTypeLabel(string $type): string
|
|
{
|
|
return match ($type) {
|
|
'ip' => __('IP Address or CIDR'),
|
|
'uri_exact' => __('Exact URI'),
|
|
'uri_prefix' => __('URI Prefix'),
|
|
'host' => __('Host Header'),
|
|
default => $type,
|
|
};
|
|
}
|
|
}
|