Add ModSecurity whitelist rules
This commit is contained in:
@@ -8,7 +8,9 @@ use App\Models\Setting;
|
|||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Forms\Concerns\InteractsWithForms;
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
use Filament\Forms\Contracts\HasForms;
|
use Filament\Forms\Contracts\HasForms;
|
||||||
@@ -47,10 +49,16 @@ class Waf extends Page implements HasForms
|
|||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->wafInstalled = $this->detectWaf();
|
$this->wafInstalled = $this->detectWaf();
|
||||||
|
$whitelistRaw = Setting::get('waf_whitelist_rules', '[]');
|
||||||
|
$whitelistRules = json_decode($whitelistRaw, true);
|
||||||
|
if (! is_array($whitelistRules)) {
|
||||||
|
$whitelistRules = [];
|
||||||
|
}
|
||||||
$this->wafFormData = [
|
$this->wafFormData = [
|
||||||
'enabled' => Setting::get('waf_enabled', '0') === '1',
|
'enabled' => Setting::get('waf_enabled', '0') === '1',
|
||||||
'paranoia' => Setting::get('waf_paranoia', '1'),
|
'paranoia' => Setting::get('waf_paranoia', '1'),
|
||||||
'audit_log' => Setting::get('waf_audit_log', '1') === '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')),
|
->label(__('Enable Audit Log')),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->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_enabled', $requestedEnabled ? '1' : '0');
|
||||||
Setting::set('waf_paranoia', (string) ($data['paranoia'] ?? '1'));
|
Setting::set('waf_paranoia', (string) ($data['paranoia'] ?? '1'));
|
||||||
Setting::set('waf_audit_log', ! empty($data['audit_log']) ? '1' : '0');
|
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 {
|
try {
|
||||||
$agent = new AgentClient;
|
$agent = new AgentClient;
|
||||||
$agent->wafApplySettings(
|
$agent->wafApplySettings(
|
||||||
$requestedEnabled,
|
$requestedEnabled,
|
||||||
(string) ($data['paranoia'] ?? '1'),
|
(string) ($data['paranoia'] ?? '1'),
|
||||||
! empty($data['audit_log'])
|
! empty($data['audit_log']),
|
||||||
|
$data['whitelist_rules'] ?? []
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! $this->wafInstalled && ! empty($data['enabled'])) {
|
if (! $this->wafInstalled && ! empty($data['enabled'])) {
|
||||||
|
|||||||
@@ -1325,12 +1325,13 @@ class AgentClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WAF / Geo
|
// 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', [
|
return $this->send('waf.apply', [
|
||||||
'enabled' => $enabled,
|
'enabled' => $enabled,
|
||||||
'paranoia' => $paranoia,
|
'paranoia' => $paranoia,
|
||||||
'audit_log' => $auditLog,
|
'audit_log' => $auditLog,
|
||||||
|
'whitelist_rules' => $whitelistRules,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3078,6 +3078,7 @@ function wafApplySettings(array $params): array
|
|||||||
$paranoia = (int) ($params['paranoia'] ?? 1);
|
$paranoia = (int) ($params['paranoia'] ?? 1);
|
||||||
$paranoia = max(1, min(4, $paranoia));
|
$paranoia = max(1, min(4, $paranoia));
|
||||||
$auditLog = !empty($params['audit_log']);
|
$auditLog = !empty($params['audit_log']);
|
||||||
|
$whitelistRules = $params['whitelist_rules'] ?? [];
|
||||||
|
|
||||||
ensureJabaliNginxIncludeFiles();
|
ensureJabaliNginxIncludeFiles();
|
||||||
|
|
||||||
@@ -3108,6 +3109,11 @@ function wafApplySettings(array $params): array
|
|||||||
'SecAction "id:900110,phase:1,t:none,pass,setvar:tx.executing_paranoia_level=' . $paranoia . '"',
|
'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");
|
file_put_contents(JABALI_WAF_RULES, implode("\n", $rules) . "\n");
|
||||||
|
|
||||||
$include = [
|
$include = [
|
||||||
@@ -3149,6 +3155,76 @@ function wafApplySettings(array $params): array
|
|||||||
return ['success' => true, 'enabled' => $enabled, 'paranoia' => $paranoia, 'audit_log' => $auditLog];
|
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
|
function geoUpdateDatabase(array $params): array
|
||||||
{
|
{
|
||||||
$accountId = trim((string) ($params['account_id'] ?? ''));
|
$accountId = trim((string) ($params['account_id'] ?? ''));
|
||||||
|
|||||||
Reference in New Issue
Block a user