Fix rule parsing for single reverse proxies

This commit is contained in:
fuomag9
2026-03-04 21:16:11 +01:00
parent 77d3e35c63
commit 7341070c0d
14 changed files with 685 additions and 143 deletions

View File

@@ -2,6 +2,7 @@
import {
Box,
Button,
Checkbox,
Collapse,
Divider,
@@ -12,12 +13,22 @@ import {
Typography,
} from "@mui/material";
import GppBadIcon from "@mui/icons-material/GppBad";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { useState } from "react";
import { type WafHostConfig } from "@/src/lib/models/proxy-hosts";
import { WafRuleExclusions } from "./WafRuleExclusions";
type WafMode = "merge" | "override";
type EngineMode = "Off" | "DetectionOnly" | "On";
const QUICK_TEMPLATES = [
{ label: "Allow IP", snippet: `SecRule REMOTE_ADDR "@ipMatch 1.2.3.4" "id:9000,phase:1,allow,nolog,msg:'Allow IP'"` },
{ label: "Disable WAF for path", snippet: `SecRule REQUEST_URI "@beginsWith /api/" "id:9001,phase:1,ctl:ruleEngine=Off,nolog"` },
{ label: "Remove XSS rules", snippet: `SecRuleRemoveByTag "attack-xss"` },
{ label: "Block User-Agent", snippet: `SecRule REQUEST_HEADERS:User-Agent "@contains badbot" "id:9002,phase:1,deny,status:403,log"` },
];
type Props = {
value?: WafHostConfig | null;
showModeSelector?: boolean;
@@ -29,6 +40,7 @@ export function WafFields({ value, showModeSelector = true }: Props) {
const [engineMode, setEngineMode] = useState<EngineMode>(value?.mode ?? "DetectionOnly");
const [loadCrs, setLoadCrs] = useState(value?.load_owasp_crs ?? true);
const [customDirectives, setCustomDirectives] = useState(value?.custom_directives ?? "");
const [showTemplates, setShowTemplates] = useState(false);
return (
<Box
@@ -204,6 +216,11 @@ export function WafFields({ value, showModeSelector = true }: Props) {
}
/>
{/* Excluded rule IDs */}
<Box mt={1.5}>
<WafRuleExclusions value={value?.excluded_rule_ids} />
</Box>
{/* Custom directives */}
<Box mt={1.5}>
<TextField
@@ -219,6 +236,34 @@ export function WafFields({ value, showModeSelector = true }: Props) {
fullWidth
/>
</Box>
{/* Quick Templates */}
<Box mt={0.5}>
<Button
size="small"
endIcon={<ExpandMoreIcon sx={{ transform: showTemplates ? "rotate(180deg)" : "none", transition: "transform 0.2s" }} />}
onClick={() => setShowTemplates((v) => !v)}
sx={{ color: "text.secondary", textTransform: "none", px: 0 }}
>
Quick Templates
</Button>
<Collapse in={showTemplates}>
<Stack spacing={0.75} mt={0.75}>
{QUICK_TEMPLATES.map((t) => (
<Button
key={t.label}
size="small"
variant="outlined"
startIcon={<ContentCopyIcon fontSize="inherit" />}
onClick={() => setCustomDirectives((prev) => prev ? `${prev}\n${t.snippet}` : t.snippet)}
sx={{ justifyContent: "flex-start", textTransform: "none", fontFamily: "monospace", fontSize: "0.72rem" }}
>
{t.label}
</Button>
))}
</Stack>
</Collapse>
</Box>
</Box>
</Collapse>
</Box>

View File

@@ -0,0 +1,63 @@
"use client";
import { Box, Chip, IconButton, Stack, TextField, Typography } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import { useState } from "react";
type Props = {
value?: number[];
};
export function WafRuleExclusions({ value }: Props) {
const [ids, setIds] = useState<number[]>(value ?? []);
const [inputVal, setInputVal] = useState("");
function addId() {
const n = parseInt(inputVal.trim(), 10);
if (!Number.isInteger(n) || n <= 0) return;
if (ids.includes(n)) { setInputVal(""); return; }
setIds((prev) => [...prev, n]);
setInputVal("");
}
function removeId(id: number) {
setIds((prev) => prev.filter((x) => x !== id));
}
return (
<Box>
<input type="hidden" name="waf_excluded_rule_ids" value={JSON.stringify(ids)} />
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ textTransform: "uppercase", letterSpacing: 0.5 }}>
Excluded Rule IDs
</Typography>
<Typography variant="caption" color="text.secondary" display="block" mb={0.75}>
Rules listed here are disabled via <code>SecRuleRemoveById</code>
</Typography>
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" gap={0.75} mb={ids.length ? 1 : 0}>
{ids.map((id) => (
<Chip
key={id}
label={id}
size="small"
onDelete={() => removeId(id)}
sx={{ fontFamily: "monospace", fontSize: "0.75rem" }}
/>
))}
</Stack>
<Stack direction="row" spacing={1} alignItems="center" sx={{ maxWidth: 260 }}>
<TextField
size="small"
label="Rule ID"
value={inputVal}
onChange={(e) => setInputVal(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addId(); } }}
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
sx={{ flex: 1 }}
/>
<IconButton size="small" onClick={addId} color="primary">
<AddIcon fontSize="small" />
</IconButton>
</Stack>
</Box>
);
}