Fix rule parsing for single reverse proxies
This commit is contained in:
@@ -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>
|
||||
|
||||
63
src/components/proxy-hosts/WafRuleExclusions.tsx
Normal file
63
src/components/proxy-hosts/WafRuleExclusions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user