Add ModSecurity whitelist rules
This commit is contained in:
@@ -8,7 +8,9 @@ use App\Models\Setting;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
@@ -47,10 +49,16 @@ class Waf extends Page implements HasForms
|
||||
public function mount(): void
|
||||
{
|
||||
$this->wafInstalled = $this->detectWaf();
|
||||
$whitelistRaw = Setting::get('waf_whitelist_rules', '[]');
|
||||
$whitelistRules = json_decode($whitelistRaw, true);
|
||||
if (! is_array($whitelistRules)) {
|
||||
$whitelistRules = [];
|
||||
}
|
||||
$this->wafFormData = [
|
||||
'enabled' => Setting::get('waf_enabled', '0') === '1',
|
||||
'paranoia' => Setting::get('waf_paranoia', '1'),
|
||||
'audit_log' => Setting::get('waf_audit_log', '1') === '1',
|
||||
'whitelist_rules' => $whitelistRules,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -101,6 +109,44 @@ class Waf extends Page implements HasForms
|
||||
->label(__('Enable Audit Log')),
|
||||
])
|
||||
->columns(2),
|
||||
Section::make(__('Whitelist Rules'))
|
||||
->description(__('Exclude trusted traffic from specific ModSecurity rule IDs.'))
|
||||
->schema([
|
||||
Repeater::make('whitelist_rules')
|
||||
->label(__('Whitelist Entries'))
|
||||
->schema([
|
||||
TextInput::make('label')
|
||||
->label(__('Name'))
|
||||
->placeholder(__('Admin API allowlist'))
|
||||
->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(),
|
||||
Toggle::make('enabled')
|
||||
->label(__('Enabled'))
|
||||
->default(true),
|
||||
])
|
||||
->itemLabel(fn (array $state): ?string => $state['label'] ?? $state['match_value'] ?? null)
|
||||
->addActionLabel(__('Add Whitelist Rule'))
|
||||
->collapsible()
|
||||
->columns(['default' => 2, 'md' => 2]),
|
||||
])
|
||||
->columns(1),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -115,13 +161,15 @@ class Waf extends Page implements HasForms
|
||||
Setting::set('waf_enabled', $requestedEnabled ? '1' : '0');
|
||||
Setting::set('waf_paranoia', (string) ($data['paranoia'] ?? '1'));
|
||||
Setting::set('waf_audit_log', ! empty($data['audit_log']) ? '1' : '0');
|
||||
Setting::set('waf_whitelist_rules', json_encode(array_values($data['whitelist_rules'] ?? []), JSON_UNESCAPED_SLASHES));
|
||||
|
||||
try {
|
||||
$agent = new AgentClient;
|
||||
$agent->wafApplySettings(
|
||||
$requestedEnabled,
|
||||
(string) ($data['paranoia'] ?? '1'),
|
||||
! empty($data['audit_log'])
|
||||
! empty($data['audit_log']),
|
||||
$data['whitelist_rules'] ?? []
|
||||
);
|
||||
|
||||
if (! $this->wafInstalled && ! empty($data['enabled'])) {
|
||||
|
||||
@@ -1325,12 +1325,13 @@ class AgentClient
|
||||
}
|
||||
|
||||
// WAF / Geo
|
||||
public function wafApplySettings(bool $enabled, string $paranoia, bool $auditLog): array
|
||||
public function wafApplySettings(bool $enabled, string $paranoia, bool $auditLog, array $whitelistRules = []): array
|
||||
{
|
||||
return $this->send('waf.apply', [
|
||||
'enabled' => $enabled,
|
||||
'paranoia' => $paranoia,
|
||||
'audit_log' => $auditLog,
|
||||
'whitelist_rules' => $whitelistRules,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -3078,6 +3078,7 @@ function wafApplySettings(array $params): array
|
||||
$paranoia = (int) ($params['paranoia'] ?? 1);
|
||||
$paranoia = max(1, min(4, $paranoia));
|
||||
$auditLog = !empty($params['audit_log']);
|
||||
$whitelistRules = $params['whitelist_rules'] ?? [];
|
||||
|
||||
ensureJabaliNginxIncludeFiles();
|
||||
|
||||
@@ -3108,6 +3109,11 @@ function wafApplySettings(array $params): array
|
||||
'SecAction "id:900110,phase:1,t:none,pass,setvar:tx.executing_paranoia_level=' . $paranoia . '"',
|
||||
];
|
||||
|
||||
$whitelistLines = buildWafWhitelistRules($whitelistRules);
|
||||
if (!empty($whitelistLines)) {
|
||||
$rules = array_merge($rules, $whitelistLines);
|
||||
}
|
||||
|
||||
file_put_contents(JABALI_WAF_RULES, implode("\n", $rules) . "\n");
|
||||
|
||||
$include = [
|
||||
@@ -3149,6 +3155,76 @@ function wafApplySettings(array $params): array
|
||||
return ['success' => true, 'enabled' => $enabled, 'paranoia' => $paranoia, 'audit_log' => $auditLog];
|
||||
}
|
||||
|
||||
function buildWafWhitelistRules(array $rules): array
|
||||
{
|
||||
$lines = [];
|
||||
$ruleBaseId = 120000;
|
||||
$index = 0;
|
||||
|
||||
$matchMap = [
|
||||
'ip' => ['REMOTE_ADDR', '@ipMatch'],
|
||||
'uri_exact' => ['REQUEST_URI', '@streq'],
|
||||
'uri_prefix' => ['REQUEST_URI', '@beginsWith'],
|
||||
'host' => ['REQUEST_HEADERS:Host', '@streq'],
|
||||
];
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
if (!is_array($rule)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($rule['enabled']) && !$rule['enabled']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matchType = (string) ($rule['match_type'] ?? '');
|
||||
$matchValue = trim((string) ($rule['match_value'] ?? ''));
|
||||
$idsRaw = (string) ($rule['rule_ids'] ?? '');
|
||||
|
||||
if ($matchValue === '' || $idsRaw === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($matchMap[$matchType])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ids = preg_split('/[,\s]+/', $idsRaw, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
$ids = array_values(array_filter(array_map('trim', $ids), function ($id) {
|
||||
return ctype_digit($id);
|
||||
}));
|
||||
|
||||
if (empty($ids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$variable, $operator] = $matchMap[$matchType];
|
||||
$ruleId = $ruleBaseId + $index;
|
||||
$index++;
|
||||
|
||||
$ctlParts = [];
|
||||
foreach ($ids as $id) {
|
||||
$ctlParts[] = 'ctl:ruleRemoveById=' . $id;
|
||||
}
|
||||
|
||||
$value = str_replace('"', '\\"', $matchValue);
|
||||
$lines[] = sprintf(
|
||||
'SecRule %s "%s %s" "id:%d,phase:1,pass,nolog,%s"',
|
||||
$variable,
|
||||
$operator,
|
||||
$value,
|
||||
$ruleId,
|
||||
implode(',', $ctlParts)
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($lines)) {
|
||||
array_unshift($lines, '# Whitelist rules (managed by Jabali)');
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
function geoUpdateDatabase(array $params): array
|
||||
{
|
||||
$accountId = trim((string) ($params['account_id'] ?? ''));
|
||||
|
||||
Reference in New Issue
Block a user