Skip to content

waf: managed rules (OWASP Core Rule Set) card, feature-flag pre-flight, CRS column#331

Open
acoshift wants to merge 2 commits into
mainfrom
waf-managed-rules
Open

waf: managed rules (OWASP Core Rule Set) card, feature-flag pre-flight, CRS column#331
acoshift wants to merge 2 commits into
mainfrom
waf-managed-rules

Conversation

@acoshift

@acoshift acoshift commented Jul 11, 2026

Copy link
Copy Markdown
Member

What

The console slice of WAF managed rules (OWASP Core Rule Set) — see SPEC-waf-managed-rules.md §5 (console UX) and §14 row 4. The WAF zone gains a typed managedRules block (waf.set/waf.get, whole-replace); this PR is the UI for it. No SecLang anywhere in the console — generation is deployer-side (§4.2); the API surface is typed knobs only.

  • Manage page (waf/manage/+page.svelte): a Managed rules card between the rules list and rate limits — enable toggle, Enforce/Detect mode (with the v1 "detect matches are ops-visible only" caveat verbatim from §5), paranoia level 1–4 segmented picker, anomaly threshold, and an excluded-CRS-rule-id chip editor with client-side range validation (911100–948999, max 50). Toggling off keeps the tuning visible-but-frozen ("Disabled — your tuning is kept for re-enable", §2.2 semantics).
  • Whole-replace safety: every zone rewrite echoes the stored managed block verbatim — the manage page's rule/limit inline edits echo the last-saved block (a dirty card edit never rides along), and the rule and limit editor pages (waf/edit, waf/limit) echo data.zone.managedRules on save. Omitting the field means "disabled and cleared" server-side, so a missing echo would destroy tuning on any unrelated edit.
  • Feature-flag pre-flight: the manage loader fetches location.get and gates the card on features.waf.managedRules — unsupported locations render "Not available in this location" instead of failing on save. If an un-flagged location still carries a stored enabled block (ops un-flag, or a block set via CLI), the card shows the stored state read-only and offers Disable managed rules (sends enabled:false with tuning kept — the one write the server gate accepts; without it every rule/limit save in that location would be rejected).
  • Firewall index (waf/+page.svelte): CRS column (on / off / —), mirroring the api Table() column.
  • $lib/waf/managed.ts: form ⇄ api normalization beside limits.ts; exclusions dedupe + ascending sort so a rule-only save echoing the server block stays byte-identical (supports the apiserver DeepEqual no-op guard, §3.3).
  • TagInput gains a real disabled prop (inner input + chip-remove buttons) so the frozen exclusion list is keyboard-frozen too, not just pointer-events: none.
  • Types (api.d.ts), dev mock (session-mutable managed block so enable → save → reload round-trips offline), Playwright spec tests/waf-managed.spec.js (12 cases).

Deliberate deviation from spec letter: §5 says the Enabled toggle itself goes through GuardedButton/can('waf.set'); here the toggle is a plain checkbox and only Save managed rules is permission-guarded — matching this page's existing convention (rule/limit drafts are also freely editable with a guarded submit). The write is gated either way.

Tests

  • bun run lint / bun run check: 0 errors.
  • bun run test: 343 passed (full suite), including tests/waf-managed.spec.js 12/12: unavailable state, enable/tune/save payload, detect mode, stored round-trip, disabled-but-tuned freeze (incl. keyboard), exclusion range validation, whole-replace echo from the manage page AND both editor pages, never-configured omission, un-flagged-location disable exit, CRS column.

Rollout (spec §10; this PR is step 4 — deploy last)

Everything gates on a parapet release: moonrhythm/parapet-ingress-controller#181 (unconditional phase-2 evaluation, zone label on parapet_coraza_matches, resolving Include forms + CRS compile test) must be merged, released, and rolled out with CORAZA_ENABLED=true (+ CORAZA_REQUEST_BODY_LIMIT) on the flagged clusters before any deploys-side deploy — a merely Coraza-capable image is not sufficient (§10 step 0).

Order: parapet release + CORAZA_ENABLEDapi PR → deployer binary → apiserver (migration managed_rules column, then binary) → flip features.waf.managedRules per location (ops SQL, by name) → console (this PR) / docs / mcp / CLI api re-pin, any order.

Safe to merge/deploy early regardless: pre-flag no location advertises features.waf.managedRules, so the card renders "Not available in this location" everywhere and the editors' managedRules echo is undefined (field omitted) for zones that have no block — byte-identical waf.set payloads against an old apiserver.

The deploys CLI re-pin gate (§6) applies to the CLI, not this repo: deploys must re-pin the api module before waf set -f zone.yaml passes the field through; until then CLI zone rewrites drop managed rules the same way the docs warning (§7) describes for scripts.

Expected conflicts with sibling WAF branches

Sibling branches in flight (api/apiserver: waf-test, waf-ip-lists, waf-events; console worktrees: waf-copy-zone, waf-events, waf-ip-lists, waf-test) overlap this diff; first merged wins, the rest rebase with semantic resolution:

  • waf/manage/+page.svelte — touched by all four siblings. Resolution: keep each feature's card/section additive; any zone rewrite they add must echo managedRules (the persistZone(nextRules, nextLimits, nextManaged) helper here is the pattern to adopt).
  • waf/edit/+page.svelte, waf/limit/+page.sveltewaf-test touches both, exactly where this PR adds the managedRules echo to the waf.set body. Resolution: keep the echo line; it must survive any refactor of the save body.
  • waf/+page.svelte — this PR widens the table to 8 columns (EmptyRow/ErrorRow colspan 7→8). waf-ip-lists/waf-copy-zone adding columns is a semantic conflict: recount the colspan, don't take either side textually.
  • mock.ts / api.d.ts — three branches touch them; unions are additive, merge both sides.
  • waf-copy-zone's cross-location copy modal must decide copy-vs-drop for managedRules and handle a target location without the flag (same hazard class as the editor-echo fix in this PR): suggested resolution — copy the block with enabled:false when the target lacks features.waf.managedRules, copy verbatim otherwise.

Screenshots

Generated via the Playwright mock harness (bun run test, mock fixtures), published on the screenshots branch.

managed rules card (enabled + tuned) disabled but tuned (frozen)
managed card disabled but tuned
un-flagged location with stored enabled block (disable exit) firewall index CRS column
unavailable with exit index CRS column

@deploys-app

deploys-app Bot commented Jul 11, 2026

Copy link
Copy Markdown

Preview console-pr-331

URL: https://console-pr-331-606515731026706458.rcf2.deploys.app

Image: registry.deploys.app/deploys-app/console@sha256:43d0480f4de26f8d29aa3768f921c77e7049ecf22034093df367f6307da75ba8

Commit: 4375f50

updated 2026-07-11T06:19:19Z

acoshift added 2 commits July 11, 2026 13:17
The WAF zone gains a managedRules block (apiserver waf.set/waf.get); this is
the console leg:

- Manage page: a Managed rules card between the rules list and rate limits —
  enable toggle, enforce/detect mode, paranoia level 1-4 segmented picker,
  anomaly threshold, and an excluded-CRS-rule-id chip editor with client-side
  range validation (911100-948999, max 50). Saving echoes the whole zone
  (rules + limits + managed block) per waf.set's whole-replace contract, and
  unrelated rule/limit writes echo the last-SAVED managed block so a dirty
  card edit never rides along. Toggling off keeps the tuning visible
  (persisted server-side for re-enable).
- Loader pre-flight: location.get gates the card on
  features.waf.managedRules — unsupported locations render a disabled
  "Not available in this location" state instead of failing on save.
- Firewall index: CRS column (on / off / —) mirroring the api Table() column.
- $lib/waf/managed.ts: form <-> api normalization beside limits.ts.
- api.d.ts: WafManagedRules, WafZone.managedRules, Location features.waf
  widened to { managedRules?: boolean }.
- Dev mock: seed location flags managedRules, seed zone carries a
  session-mutable block so enable -> save -> reload round-trips offline;
  location.get accepts the id arg the console actually sends.
- Playwright: tests/waf-managed.spec.js covers the unavailable state, enable/
  tune/save payload, detect mode, round-trip, disabled-but-tuned freeze,
  exclusion validation, whole-replace echo/omission, and the CRS column.
…n exit

Review fixes on the managed-rules card:

- The rule and limit editor pages rewrite the WHOLE zone on save but omitted
  managedRules — under waf.set's whole-replace contract that silently disabled
  AND cleared the stored block (tuning destroyed) on any rule/limit edit. Both
  pages now echo the loaded zone's block verbatim, same as the manage page's
  persistZone. One Playwright echo case per page.
- A location can lose the features.waf.managedRules flag while a zone still
  carries an ENABLED block (ops un-flag, or a block set via CLI); the server
  rejects any save echoing enabled:true there, leaving the manage page
  unsaveable. The unavailable state now shows the stored block read-only and,
  when it is enabled, offers "Disable managed rules" (enabled:false with
  tuning kept — the one write the gate accepts).
- The frozen excluded-rules list was only mouse-frozen (pointer-events); the
  TagInput now takes a real disabled prop (inner input + chip-remove buttons),
  so keyboard users can't edit the kept tuning while the block is off.
@acoshift acoshift force-pushed the waf-managed-rules branch from 3084a1d to 4375f50 Compare July 11, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant