diff --git a/src/components/proxy-hosts/WafFields.tsx b/src/components/proxy-hosts/WafFields.tsx index 3a8d649b..2bfcadce 100644 --- a/src/components/proxy-hosts/WafFields.tsx +++ b/src/components/proxy-hosts/WafFields.tsx @@ -1,146 +1,225 @@ "use client"; import { - Accordion, - AccordionDetails, - AccordionSummary, Box, Checkbox, - FormControl, + Collapse, + Divider, FormControlLabel, - FormLabel, - Radio, - RadioGroup, Stack, Switch, TextField, - ToggleButton, - ToggleButtonGroup, Typography, } from "@mui/material"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import SecurityIcon from "@mui/icons-material/Security"; +import GppBadIcon from "@mui/icons-material/GppBad"; import { useState } from "react"; import { type WafHostConfig } from "@/src/lib/models/proxy-hosts"; +type WafMode = "merge" | "override"; +type EngineMode = "Off" | "DetectionOnly" | "On"; + type Props = { value?: WafHostConfig | null; + showModeSelector?: boolean; }; -export function WafFields({ value }: Props) { +export function WafFields({ value, showModeSelector = true }: Props) { const [enabled, setEnabled] = useState(value?.enabled ?? false); - const [engineMode, setEngineMode] = useState<"Off" | "DetectionOnly" | "On">( - value?.mode ?? "DetectionOnly" - ); + const [wafMode, setWafMode] = useState(value?.waf_mode ?? "merge"); + const [engineMode, setEngineMode] = useState(value?.mode ?? "DetectionOnly"); const [loadCrs, setLoadCrs] = useState(value?.load_owasp_crs ?? true); const [customDirectives, setCustomDirectives] = useState(value?.custom_directives ?? ""); - const [wafMode, setWafMode] = useState<"merge" | "override">(value?.waf_mode ?? "merge"); return ( - - }> - - - Web Application Firewall (WAF) - {enabled && ( - - {engineMode === "On" ? "Blocking" : engineMode === "DetectionOnly" ? "Detection Only" : "Off"} - - )} - - - - {/* Hidden marker so the server action knows WAF config was submitted */} - - - - - + + theme.palette.mode === "dark" ? "rgba(211,47,47,0.06)" : "rgba(211,47,47,0.04)", + p: 2, + }} + > + + + + + - - {/* Enable toggle */} + {/* Header */} + + + + + + + + Web Application Firewall + + + Inspect and block malicious requests via Coraza / OWASP CRS + + + + setEnabled(checked)} + sx={{ flexShrink: 0 }} + /> + + + {/* Expanded content */} + + + {/* Override mode selector */} + {showModeSelector && ( + <> + + {(["merge", "override"] as WafMode[]).map((v) => ( + setWafMode(v)} + sx={{ + flex: 1, + py: 0.75, + px: 1.5, + borderRadius: 1.5, + border: "1.5px solid", + borderColor: wafMode === v ? "error.main" : "divider", + bgcolor: + wafMode === v + ? (theme) => + theme.palette.mode === "dark" + ? "rgba(211,47,47,0.12)" + : "rgba(211,47,47,0.08)" + : "transparent", + cursor: "pointer", + textAlign: "center", + transition: "all 0.15s ease", + userSelect: "none", + "&:hover": { + borderColor: wafMode === v ? "error.main" : "text.disabled", + }, + }} + > + + {v === "merge" ? "Merge with global" : "Override global"} + + + ))} + + + + )} + {!showModeSelector && } + + {/* Engine mode */} + + Engine Mode + + + {(["Off", "DetectionOnly", "On"] as EngineMode[]).map((v) => ( + setEngineMode(v)} + sx={{ + flex: 1, + py: 0.75, + px: 1, + borderRadius: 1.5, + border: "1.5px solid", + borderColor: engineMode === v ? "error.main" : "divider", + bgcolor: + engineMode === v + ? (theme) => + theme.palette.mode === "dark" + ? "rgba(211,47,47,0.12)" + : "rgba(211,47,47,0.08)" + : "transparent", + cursor: "pointer", + textAlign: "center", + transition: "all 0.15s ease", + userSelect: "none", + "&:hover": { + borderColor: engineMode === v ? "error.main" : "text.disabled", + }, + }} + > + + {v === "DetectionOnly" ? "Detect only" : v} + + + ))} + + + + + {/* OWASP CRS */} setEnabled(checked)} + setLoadCrs(checked)} size="small" /> } - label="Enable WAF for this host" + label={ + + + Load OWASP Core Rule Set + + + Covers SQLi, XSS, LFI, RCE and hundreds of other attack patterns + + + } /> - {enabled && ( - <> - {/* Override mode */} - - - Override Mode - - v && setWafMode(v)} - size="small" - sx={{ mt: 0.5 }} - > - Merge with global - Override global - - - - {/* Engine mode */} - - - Engine Mode - - setEngineMode(e.target.value as "Off" | "DetectionOnly" | "On")} - > - } label="Off" /> - } label="Detection Only" /> - } label="On (Blocking)" /> - - - - {/* OWASP CRS */} - setLoadCrs(checked)} - size="small" - /> - } - label={ - - Load OWASP Core Rule Set{" "} - - (covers SQLi, XSS, LFI, RCE) - - - } - /> - - {/* Custom directives */} - setCustomDirectives(e.target.value)} - placeholder={`SecRule REQUEST_URI "@contains /secret" "id:9001,deny,status:403,log,msg:'Blocked path'"`} - inputProps={{ style: { fontFamily: "monospace", fontSize: "0.8rem" } }} - helperText="ModSecurity SecLang syntax. Applied after OWASP CRS if enabled." - fullWidth - /> - - )} - - - + {/* Custom directives */} + + setCustomDirectives(e.target.value)} + placeholder={`SecRule REQUEST_URI "@contains /secret" "id:9001,deny,status:403,log,msg:'Blocked path'"`} + inputProps={{ style: { fontFamily: "monospace", fontSize: "0.8rem" } }} + helperText="ModSecurity SecLang syntax. Appended after OWASP CRS if enabled." + fullWidth + /> + + + + ); }