Skip to content

Waf policy manager UI - #135

Open
singamL887 wants to merge 26 commits into
mainfrom
waf-policy-manager-ui
Open

Waf policy manager UI#135
singamL887 wants to merge 26 commits into
mainfrom
waf-policy-manager-ui

Conversation

@singamL887

@singamL887 singamL887 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds the WAF Policy Manager UI and its backend, letting operators manage NGINX App Protect resources on a cluster from BNK-Forge without hand-editing YAML.

Backend — thin CRUD routes over the existing generic KubernetesService CRD methods for appprotect.f5.com/v1 resources (no compile logic in Forge; compilation stays in the unmodified nap-policy-operator PLM chart):

  • routes/k8s/waf_policies.py — 17 routes covering APPolicy, APLogConf, the singleton APSignatures, and APUserSig (list / get / create / update / delete), plus a force-recompile endpoint. Reads gated by require_viewer, every mutation by require_cluster_owner.
  • routes/k8s/waf_logs.py — a security-logs endpoint that returns recent App Protect log entries, reading from ClickHouse when available and falling back to resolving the syslog endpoint (SecPolicy → F5BigLogProfile → F5BigHslPub) and reading from the receiver pod / TCP.

Frontend — a policy-creation wizard (WafPolicyWizard), structured editors for each resource type (APPolicyForm, APLogConfForm, APUserSigForm), a policy detail view (WafPolicyDetail), and a Security Logs tab (SecurityLogsTab) with filtering. A syslog-receiver deployment path backs the logs feature.

Note: later commits on this branch also grew a WAF dashboard and a Gateway WAF page (waf_dashboard.py, waf_gateway.py, waf_panels.py, dashboard-tabs, and the matching pages/hooks). These were added on top of the original Policy Manager scope; see the reviewer thread about keeping that separate.

Fixes / Implements: #136


Architectural Decision Record (ADR)

  • This PR includes or updates an ADR in docs/adr/ADR-<issue>-<title>.md (Required for non-trivial feature/architecture changes).
  • N/A (Bug fix or minor docs tweak).

ADR: not yet filed — owed by the author per AGENTS.md. The design record referenced nine times in waf_policies.py (docs/WAF_POLICY_MANAGER_DESIGN.md) and the ADR both still need to be written; the CRD scope, the "compilation stays in the PLM chart" boundary, and the singleton APSignatures handling are the decisions worth recording.


Type of Change

  • Bug fix (non-breaking change fixing an issue)
  • New feature (non-breaking change adding functionality)
  • Breaking change (fix or feature causing existing functionality to break)
  • Documentation update

Verification & Testing

  • Merged staging into the branch (was 29 commits behind) — resolves the three stale-base CI reds without touching feature code: axios ^1.18.1 / js-yaml ^4.3.1 pins and the .trivyignore CVE-2026-57433 deferral now come in from staging; package-lock.json regenerated with npm install.
  • scripts/generate-openapi.py --check — passes (573 paths, 500 schemas); openapi.json and frontend-v2/src/types/api-generated.ts regenerated after adding response_model= to the WAF routes.
  • frontend-v2: npm run build (tsc && vite build) — passes.
  • backend: pytest -k waf — 38 passed.
  • ruff check — clean on the changed backend files.

Checklist

  • My code follows the project's code style and formatting guidelines.
  • I have updated documentation where necessary.
  • I have generated updated openapi types if modifying backend routes/schemas (make openapi-types).

https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW

Migrates all WAF Policy Manager work from gitlab/f5/bnk-forge
waf-policy-manager-ui branch to this repo.

Backend:
- k8s_types.py: ApiGroups.APPPROTECT, ResourceCategory.WAF
- k8s_resource_registry.py: APPolicy, APLogConf, APSignatures, APUserSig CRDs
- routes/k8s/waf_policies.py: full CRUD for all 4 CRD types
- routes/k8s/__init__.py + main.py: register waf_policies_router
- tests/unit/test_waf_policies.py: 10 unit tests (all passing)

Frontend:
- types/waf.ts, lib/api/waf-policies.ts, hooks/useWafPolicies.ts
- lib/waf-policy-validation.ts, lib/queryKeys.ts (WAF keys)
- pages/WafPolicies.tsx: 4-tab UI — full Create/List/Edit/Delete
- components/k8s/WafPolicyWizard.tsx: 4-step policy creation wizard
- components/k8s/f5bnk-details/WafPolicyDetail.tsx: detail panel
- components/layout/Sidebar.tsx: WAF Policies nav link
- router.tsx: /waf-policies route
- types/index.ts: re-export WAF types
- .npmrc: F5 Artifactory registry

24 frontend tests + 10 backend tests — all passing; 0 WAF tsc errors
The secrets/ directory is gitignored at repo level. Inline the stub
directly in ProjectDetailV2 until a proper implementation is available.
Also removes the gitignored directory from disk.
The ?worker Vite transform syntax is not portable across build configs.
@monaco-editor/react's loader handles workers internally when given
the monaco instance; no custom MonacoEnvironment needed.
…i branch

Porting 22 commits from gitlab/f5/bnk-forge waf-policy-manager-ui to github.
All files copied verbatim — no code changes during migration.

Verified clean:
  - npx tsc --noEmit: 0 errors
  - 24 WAF frontend tests: all pass
  - 10 WAF backend unit tests: all pass
  - npm run build: success (52s)
  - make deploy: all containers healthy
  - 8 WAF API routes registered in OpenAPI

Changes ported (grouped by feature):

CORE WAF FORMS (new files: waf/ directory):
  APPolicyForm.tsx  — 8-tab full-field policy editor (all NAP spec fields, no
    raw JSON textareas); interactive widgets for every field type: ToggleList
    (violations/evasions), SigSetEditor (signature sets + action), TagInput
    (server-technologies, threat-campaigns, sensitive-parameters), KVListEditor
    (URLs/params/cookies/headers), IpListEditor (geo/IP)
  APLogConfForm.tsx — all fields on one tab; request_type card picker; API
    version selector (v1/v1beta1); live { } JSON tab with two-way sync
  APUserSigForm.tsx — identity + signatures tabs; full per-signature fields
  WafWizardFrame.tsx — shared tabbed shell with red dot validation, toolbar slot
  waf-utils.tsx     — shared validateK8sName, extractApiError helpers
  WafFormToolbar.tsx (NEW) — Clone / Drafts / Import JSON toolbar for create forms
    Clone: namespace/name picker, all CRs in namespace, fills all form fields
    Drafts: localStorage auto-save; restore deletes draft (name is taken once used)
    Import JSON: paste or file upload, applies to form fields

DETAIL PANELS (new files):
  APLogConfDetail.tsx  — Overview + Bundle Status tabs; View JSON toggle button
  APUserSigDetail.tsx  — identity + signatures detail view

DETAIL PANELS (updated):
  WafPolicyDetail.tsx  — Summary tab replaces raw JSON tab; View JSON button;
    shows enforcement mode, active features, labels/annotations
  shared.tsx           — break-all + min-w-0 on InfoRow for SHA256/location overflow

UI / UX FIXES:
  WafPolicies.tsx      — Export JSON (all CRDs), Force Recompile (policies),
    Delete APSignatures, Labels/Annotations in detail panels, RefreshButton
    component with spinner+flash, all SelectContent position=popper,
    immutable CR name in edit mode, API version hidden in edit mode
  destructive-confirm-dialog.tsx — autoFocus prevent keyboard leak from create form

BACKEND:
  waf_policies.py — DELETE /waf/signatures, POST /waf/policies/{name}/recompile

HOOKS / API:
  useWafPolicies.ts — useDeleteWafSignatures, useRecompileWafPolicy
  waf-policies.ts   — deleteSignatures, recompilePolicy client methods
  waf-drafts.ts (NEW) — localStorage draft CRUD utility

VALIDATION FIXES:
  max_request_size regex corrected to match CRD exactly (rejects 100k, 1024k)
  Cross-validation: max_message_size must not exceed max_request_size
  Stuck 'Working...' button fixed via isSubmitting state + finally{} pattern

All 24 frontend + 10 backend tests pass. tsc clean. Build succeeds.
- backend/routes/k8s/waf_policies.py: remove unused 'import time' (ruff F401)
- backend/openapi.json: regenerate spec with 8 new WAF CRD endpoints (openapi-check)
- frontend-v2/.npmrc: switch from F5 Artifactory to public npm registry (env-specific code + CI access)
- frontend-v2/package-lock.json: track lockfile in git for CI cache resolution
- .gitignore: add exception to allow frontend-v2/package-lock.json to be tracked
…Lint D-020)

Replace all raw palette colors (red-*, green-*, blue-*, amber-*, slate-*, zinc-*)
with semantic design tokens across all WAF UI files per ADR D-020:
  text-destructive / bg-destructive/10 / border-destructive/20  (errors)
  text-success / bg-success/10 / border-success/20              (success)
  text-warning / bg-warning/10 / border-warning/20              (warnings)
  text-primary / bg-primary / border-primary                    (accent)
  text-muted-foreground / text-foreground / bg-muted / border   (neutral)

Also:
- useWafPolicies.ts: replace useMutation with useAppMutation (restricted import rule)
- destructive-confirm-dialog.tsx: fix D-020 violations introduced in previous session
- waf-utils.tsx: add eslint-disable react-refresh/only-export-components (mixes
  utility functions and a JSX component; same pattern as shared.tsx, ModuleStatusBadge)
- frontend-v2/package-lock.json: regenerate with registry.npmjs.org URLs
  (old lockfile had all packages resolved to F5 Artifactory, causing
  npm ci to crash with "Exit handler never called!" in GitHub Actions)
- frontend-v2/src/types/api-generated.ts: regenerate TypeScript types from
  updated openapi.json (new WAF endpoints were added but types were stale)
@jgruberf5

Copy link
Copy Markdown
Collaborator

Review of #135 — WAF Policy Manager UI

Thanks for this — it is a substantial, coherent feature, and the backend route module in particular is written with care. Reviewed the full diff: 40 files, +15,148/-23.

The blocking items below are almost all process and base-branch issues rather than problems with your feature code. Getting them sorted should also turn CI green, since none of the three current failures are caused by your changes.

I have filed #136 to track the feature itself, tagged enhancement. Please link it from the PR description.


1. The PR template was submitted unfilled — please complete it

The description is still the template verbatim:

  • Description — placeholder text ("A clear and concise description of the changes in this Pull Request.")
  • Fixes / Implements: #[Issue Number] — placeholder, so nothing links the work to an issue. Use WAF Policy Manager: manage App Protect CRDs (APPolicy, APLogConf, APUserSig, APSignatures) from Forge #136.
  • Type of Change — nothing ticked. This is New feature.
  • ADR — neither box ticked. See §4.
  • Verification & Testing — the template's numbered boilerplate, not a description of what you actually ran.
  • Checklist — all four unticked, including the openapi one you did satisfy (see §7).

For a 15,000-line change this matters more than usual: a reviewer opening this PR has no statement of scope, no record of what was tested, and no issue to read for context. Please fill it in.

2. Base branch is main; it should be staging

waf-policy-manager-ui -> main. Work on this repo lands on staging first. main is currently 29 commits behind, and targeting it is the direct cause of everything in §3.

3. All three CI failures come from the stale base, not from your code

FAIL  P4 · Security Audit
FAIL  P4 · Docker Build + Scan
FAIL  CI Gate

P4 · Security Audit fails on pip-audit:

Found 22 known vulnerabilities, ignored 1 in 3 packages
gitpython 3.1.50  GHSA-2f96-g7mh-g2hx  → 3.1.51
...
gitpython 3.1.50  GHSA-hh9p-6wh2-4mfc  → 3.1.58

These are backend Python dependencies your PR never touches. The reason they fire:

branch pin
main gitpython==3.1.50
staging gitpython==3.1.58

staging already carries the fix version pip-audit is asking for. PRs based on staging pass both P4 jobs. Retargeting to staging should clear all three failures without you changing a line.

4. Design record is missing, and the code points at a file that does not exist

backend/routes/k8s/waf_policies.py says "See docs/WAF_POLICY_MANAGER_DESIGN.md for the full design" — referenced nine times across the PR. That file is not in this PR and does not exist in the repo, so every one of those references is currently dangling.

The template also asks for an ADR at docs/adr/ADR-<issue>-<title>.md for non-trivial feature work; this qualifies at 20 endpoints, four new CRD registrations, and a new UI section. Please land the design document with the code — the CRD scope, the "compilation stays in the PLM chart, no compile logic in Forge" boundary, and the singleton APSignatures handling are all decisions worth recording.

5. The lockfile change collides with a deliberate decision already made on staging

This PR adds !frontend-v2/package-lock.json to .gitignore and commits a new 7,574-line lockfile.

staging already solved this, in #121, and left the reasoning in .gitignore:

# package-lock.json is intentionally TRACKED — `npm ci` and the CI setup-node
# cache both require it. Ignoring it silently drops the lockfile from any export
# built off the working tree, which breaks every frontend CI job.

It tracks a 10,706-line lockfile. Yours is ~3,100 lines smaller, i.e. a materially different dependency resolution. If this merged as-is it would silently re-pin the frontend tree for everyone. On rebase to staging, please drop both the .gitignore edit and your lockfile and keep the tracked one, regenerating only if a dependency genuinely changed.

Related: npm ci in this PR reports 6 vulnerabilities (1 low, 4 moderate, 1 high) against the new lockfile — worth a look once you are on staging's.

6. Monaco worker configuration removed — unrelated to this feature, and the stated reason does not hold here

frontend-v2/src/main.tsx drops:

import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
self.MonacoEnvironment = { getWorker() { return new editorWorker() } }

justified as "The ?worker Vite syntax is not compatible with all build configurations."

staging carries that exact import today and P2 · Build · Frontend passes — so whatever configuration this was hitting, it is not this repo's build. Credit where due: you kept loader.config({ monaco }), so the editor core still loads from the local bundle and the air-gapped CDN fix is intact. What is lost is the dedicated web worker: Monaco falls back to its default worker handling, which typically means language services run on the main thread.

Either way this is a change to shared editor infrastructure with no connection to WAF policies. Please split it out with its own reasoning, or drop it.

7. Test coverage does not reach any of the 20 endpoints

backend/tests/unit/test_waf_policies.py (78 lines) covers the resource registry (5 tests) and two helpers, _build_resource_yaml and _find_by_name (5 tests). Useful, but no test exercises a single route handler — no RBAC coverage (viewer vs cluster-owner), no create/update/delete, no error paths, and nothing pinning the resourceVersion-required-on-update behaviour your own docstring documents as a live-cluster 422.

The convention here is backend/tests/integration/test_routes_*.py for route-level tests; there are plenty to copy from.

Frontend is thinner than it looks too — 344 test lines against roughly 4,500 lines of new UI.

8. Minor

  • frontend-v2/vite.config.ts — a single added blank line. Please revert; it is diff noise.
  • frontend-v2/.npmrc — sets registry=https://registry.npmjs.org/, which is already npm's default. Harmless, but it is a new config file doing nothing.

Checked and found correct

Recording these so they are not re-litigated:

  • RBAC is right. Writes take user: User = Depends(require_cluster_owner), which resolves the path's cluster_id to its project and checks ownership. Reads use dependencies=[Depends(require_viewer)], matching every existing k8s read route in routes/k8s/resources.py.
  • Generated types were regeneratedbackend/openapi.json (+1,026) and frontend-v2/src/types/api-generated.ts (+782) are both present and consistent. This is the checklist item you left unticked but did do.
  • .npmrc carries no credentials — registry line only.
  • The route module reuses the generic CRD methods rather than forking _resources.py, and documents why resourceVersion is set on update. Good.

Suggested order

  1. Retarget to staging and rebase (clears §3, and §5 becomes a delete)
  2. Fill in the PR description and link WAF Policy Manager: manage App Protect CRDs (APPolicy, APLogConf, APUserSig, APSignatures) from Forge #136 (§1)
  3. Land the design doc / ADR (§4)
  4. Split out or drop the Monaco change (§6)
  5. Add route-level tests (§7)
  6. Revert the vite blank line (§8)

@mwiget mwiget left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — Waf policy manager UI @ 7229cd0

Substantial feature — 17 backend routes, a wizard, forms and detail views. The route layer follows the house conventions well, but I can't approve this yet: three checks are red, and there are two things to settle before the code review is worth doing in depth.

Both CI failures are staleness, not defects — and the fix is the same for both

The branch is 29 commits behind staging (8 commits of its own). Both failures come from what it is missing, so neither needs a code fix:

  • P4 · Docker Build + ScanTrivy found CRITICAL CVE(s): CVE-2026-57433 (perl Storable). staging's .trivyignore carries a documented deferral for exactly this CVE (lines 109-120, tracked as #104); this branch's copy stops at line 108.
  • P4 · Security Auditnpm audit reports 1 high. staging has js-yaml ^4.3.1 and axios ^1.18.1; this branch still sees ^4.2.0 / ^1.17.0, the pre-bump versions.

Good news on the second: this PR does not modify package.json, so those are just staging moving ahead — a merge keeps the newer pins and nothing gets reverted. I ran the merge to be sure:

post-merge:  axios ^1.18.1   js-yaml ^4.3.1
conflicts:   .gitignore, frontend-v2/package-lock.json

Two conflicts to expect. Resolve package-lock.json by taking staging's package.json and regenerating with npm install rather than hand-merging 7574 lines; .gitignore is a one-liner (and note frontend-v2/package-lock.json is already tracked on staging, so the ! un-ignore is a clarification, not a policy change — that part is fine).

🔴 The Monaco worker configuration is removed, undoing a deliberate air-gap fix

frontend-v2/src/main.tsx:26 — commit 69a080c deletes self.MonacoEnvironment and the editor.worker?worker import. The code it removes carries a comment naming the reason it exists: "Without this, @monaco-editor/react fetches from jsDelivr which fails in air-gapped environments or behind firewalls." I grepped the branch: no MonacoEnvironment, getWorker or worker import remains anywhere.

To be precise about the consequence, since the replacement comment overstates it: loader.config({ monaco }) is retained, and that is what keeps monaco's core local — so this is not a straight return to CDN loading. What is gone is the web worker wiring, which is a separate mechanism the loader does not supply. Without it monaco cannot spawn its language worker and degrades to the main thread. For a product installed into customer clusters and documented for offline install, silently changing that behaviour needs to be a decision, not a side effect.

The commit message says the ?worker syntax "is not compatible with all build configurations", which reads as a build error worked around by deletion. If the build broke, the fix belongs in vite.config.ts (e.g. worker: { format: 'es' } or monaco-editor-vite-plugin) — and either way this is unrelated to WAF policies and should not ride along in this PR.

🟠 The PR description is the unedited template

Every checkbox is unticked, Fixes / Implements: #[Issue Number] is still a placeholder, and the description reads "A clear and concise description of the changes in this Pull Request."

For a 15k-line feature this is the blocking process item, not a formality. AGENTS.md puts non-trivial features on ADR → GitHub issue → roadmap → PR, and the template's own ADR checkbox is the reminder. Without a linked issue or ADR there is no record of what was decided, no roadmap entry, and a reviewer has to reconstruct intent from 40 files. Please fill it in and link the issue/ADR — that changes how the rest of the review can be done.

🟡 None of the 17 new routes declare response_model

backend/routes/k8s/waf_policies.pygrep -c response_model returns 0 across 17 routes, while AGENTS.md asks for @handle_route_errors(...) and response_model=. The decorators and auth are right (require_viewer on reads, require_cluster_owner on every mutation — good), so this is the one convention missed.

It has a downstream cost beyond style: openapi.json grows 1026 lines of untyped responses, and api-generated.ts inherits that, so the 782 lines of generated types this PR adds are weaker than they look. Honest context — dpf.py, tmm_debug.py and tunnels.py also carry none, so this is not unprecedented; it is just the largest addition of untyped routes so far.

⚪ Unrelated changes worth splitting or explaining

  • components/ui/destructive-confirm-dialog.tsxautoFocus added to a shared primitive used well beyond WAF.
  • hooks/useTaskWebSocket.tssetTimeoutwindow.setTimeout. Fine as a typing fix, unrelated here.
  • vite.config.ts — a blank line only.

Requesting changes. Concretely: merge staging (clears both red checks), restore or deliberately replace the Monaco worker setup, and fill in the description with the issue/ADR link. I'll do a full pass over the wizard, forms and route logic once it's green and the intent is written down — the structure looks reasonable and I'd rather review it against a stated design than guess at one.

Comment thread frontend-v2/src/main.tsx Outdated
// Monaco Editor: configure local bundling instead of CDN.
// @monaco-editor/react handles workers internally via its own loader.
// The ?worker Vite syntax is not compatible with all build configurations;
// worker setup is deferred to the loader's built-in mechanism instead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loader.config({ monaco }) below keeps monaco's core local, so this is not a straight return to CDN loading — but the loader does not supply the web worker, which is what the deleted self.MonacoEnvironment / editor.worker?worker block provided. I grepped the branch and no MonacoEnvironment, getWorker or worker import remains, so monaco can no longer spawn its language worker and falls back to the main thread.

The code removed here carried its own justification — "fails in air-gapped environments or behind firewalls" — so this is worth being deliberate about rather than a build-error workaround. If ?worker broke the build, the fix belongs in vite.config.ts (worker: { format: 'es' }, or a monaco vite plugin).

Either way it is unrelated to WAF policies and would be better split out.

# ============================================================================

@router.get(
"/k8s/clusters/{cluster_id}/waf/policies",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the 17 routes in this file declare response_model= (grep -c response_model -> 0). AGENTS.md asks for it alongside @handle_route_errors, which you do have on every route — as well as require_viewer on reads and require_cluster_owner on mutations, which is right.

The cost is downstream: the 1026 lines added to openapi.json describe untyped responses, so the 782 lines of api-generated.ts this PR ships are weaker than they appear. Declaring the shapes here is what makes the generated client types useful.

Context in fairness: dpf.py, tmm_debug.py and tunnels.py also declare none, so this follows a local precedent rather than inventing one.

@mwiget

mwiget commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Filed the Monaco worker point from my review as #137, so it survives independently of what happens to this PR.

Two things I checked after posting the review, both of which shape the ask:

  • loader.config({ monaco }) is retained here, so monaco's core stays local — this is not a straight return to CDN loading, and the air-gap failure quoted in the old comment does not return verbatim. What is lost is the web worker, which the loader does not supply, so language services fall back to the main thread.
  • The ?worker import builds fine in this repo's CI: P2 · Build · Frontend is green on staging (7ece9b04) and on Refuse project deletion while modules still own cloud resources #129 (10ea356a), both of which carry it. So the removal was not needed to get CI green here.

If a real build does break for you, could you say which command and environment, and the error? That determines whether the fix is a vite.config.ts change rather than dropping the configuration — and it is the one piece I could not reconstruct from the branch.

When a policy CR has a syslog endpoint configured, WAF security events
are visible in the UI through a "Security Logs" tab added to:
  - APPolicy detail panel (WAF Policies page → Policies tab → click row)
  - SecPolicy detail panel (F5 BNK page → SecPolicy resources → click row)

Backend (backend/routes/k8s/waf_logs.py):
  GET /api/k8s/clusters/{id}/waf/security-logs
  - Resolves the syslog endpoint chain automatically from the cluster CRDs:
    SecPolicy → F5BigLogProfile (publisher field) → F5BigHslPub → pool[].endpoint[]
  - Connects to the resolved host:port via TCP and reads the syslog stream
  - Parses NAP key=value log format into structured JSON entries
  - Filters by cr_kind/cr_name (policy_name field in the log stream)
  - Returns structured entries with: date_time, outcome, attack_type,
    violation_rating, violations, sig_ids, client_ip, uri, vs_name,
    support_id, request_status, unit_hostname
  - Graceful degradation: returns warning when no syslog endpoint is
    configured instead of erroring

Frontend:
  - SecurityLogsTab component: filter bar (outcome, attack_type, limit),
    log table with colour-coded outcome badges, row expand for full entry,
    CSV export, manual refresh with timestamp
  - useWafLogs hook: fetches on mount + on manual refresh, 30s auto-poll
  - waf-logs API client: typed against generated OpenAPI types
  - shared.tsx: added optional clusterId to DetailPanelProps
  - F5BNKDetailPanel / F5BNK: passes clusterId through to detail components
  - resource-registry.ts: DetailPanelProps extended with clusterId

Design notes:
  - Syslog destination is auto-resolved from CRDs — no manual host:port entry
  - Tab is present even when no syslog is configured; shows actionable warning
  - Log differentiation by vs_name/policy_name — handles multiple CRs sharing
    one syslog server correctly
  - Tested: 12 new frontend unit tests, 10 new backend unit tests all pass
Deployed a fluentd-based syslog receiver (TCP port 514) inside the cluster:
- ConfigMap: waf-syslog-receiver-config (fluentd TCP source + file output)
- Deployment: waf-syslog-receiver (f5-toda-fluentd image already in cluster)
- Service: waf-syslog-receiver (ClusterIP:514 + NodePort:30840 for backend access)
- F5BigHslPub: waf-syslog-publisher (pool endpoint: node-ip:30840)
- F5BigLogProfile: waf-log-profile (publisher: waf-syslog-publisher)
Logs written to /var/log/waf-syslog/security.<date>.log in the receiver pod.

Backend fix (waf_logs.py):
- Use `find` instead of `sh -c ls` for log file discovery (no shell in container)
- Sort candidates alphabetically so most recent date-based file is picked last
- Remove unused datetime import (ruff F401)
- Full resolution chain working: F5BigLogProfile → F5BigHslPub → host:port
- Pod exec reads log file via kubectl exec → tail, falls back to TCP stream
- Filter by policy_name field in log entries for APPolicy CR context

Verified end-to-end in the UI:
- Security Logs tab in my-waf-policy detail shows 9 parsed entries
- Endpoint auto-resolved: syslog: 10.101.0.2:30840
- All attack types shown: SQL Injection, XSS, Path Traversal, Command Injection
- PASSED requests shown with green badge (rating 1)
- Row expand shows all NAP fields: attack_type, ip_client, policy_name,
  sig_ids, sig_names, support_id, unit_hostname, uri, violation_rating, vs_name

@mwiget mwiget left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review @ c91f84f — still requesting changes

None of the four blocking items from my 7229cd0 review have been addressed. The two commits added since (28b530c, c91f84f) instead grew the PR by ~1,300 lines of new feature scope (security-logs tab + syslog-receiver deployment).

Prior blocker Status now
🔴 Monaco worker wiring removed (air-gap regression) Not fixedmain.tsx still has only loader.config({ monaco }); no MonacoEnvironment / editor.worker?worker.
🟠 PR description is the unedited template Not fixed — still "A clear and concise description…", Fixes: #[Issue Number], every checkbox blank.
🟡 No response_model on the routes Regressed further — still 0 in waf_policies.py; the new waf_logs.py adds more routes, also 0.
🔴 CI red (staleness) Still red on the same 3 checks (CI Gate, Docker Build + Scan, Security Audit), confirmed on head c91f84f — Security Audit reports 1 high npm + gitpython CVE-2026-73620.

One positive note on the new code: the added waf_logs.py pod-exec path uses argv lists (command=["find", …], command=["tail", …]) rather than a shell, and the tail target comes from find output rather than user input — so no obvious command-injection surface, and reads are gated by require_viewer. But that new syslog-receiver deployment path is exactly the kind of scope I said I'd review after the PR is green and has a stated design; it shouldn't be expanding while the blockers sit.

Recommendation, unchanged and now more pointed: please stop adding features. Get CI green, restore (or deliberately justify in vite.config.ts) the Monaco worker wiring, and fill in the description with a linked issue/ADR. Adding a syslog receiver on top of an already-blocked 16k-line PR makes it harder to land, not easier.

jgruberf5 pushed a commit that referenced this pull request Aug 19, 2026
…ywords

Review finding (mwiget): merging #158 as it stood would have closed #94
and #128 -- not because of the regex change, but because the PR's own
description DOCUMENTS closing keywords in backticks, and the parser reads
body text raw. Reproduced against this branch's real parse step with this
PR's body as PR_BODY:

    Parsed closing-keyword issues: ['94', '128', '7']

#94 and #128 are open with their real fixes unmerged in #157/#156; they
would have closed with "Auto-closed by PR #158", wrong issue and wrong PR.
The old parser had the same blind spot (it read ['94'] from this body);
widening the skip tripled the blast radius on a body that talks about the
very forms it now accepts. A parser PR is the right place to close the
class, not the instance.

Fenced blocks are stripped first (they may contain backticks), then inline
spans. Re-ran the full matrix through the real step: every real closing
line in plain text still closes; every example in backticks or a fence no
longer does; a body with both a real "Fixes #94" and a documented
"`Fixes #999`" closes only 94. This PR's own body now yields no issues.

Then ran EVERY open PR's actual body through the patched step -- the
check I should have done the first time:

    #156 -> 128   #157 -> 94   #159 -> 154   #160 -> 99
    #158 -> (none)   #161 -> (none, deliberate Refs #79)   #135 -> (none)

Each PR closes exactly its own issue and nothing else.

Claude-Session: https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4
singamL887 and others added 13 commits August 21, 2026 01:22
…lor map

NAP uses REJECTED (not BLOCKED) as the outcome value for blocked requests.
- SecurityLogsTab: rename BLOCKED dropdown option to REJECTED
- SecurityLogsTab: add REJECTED key to OUTCOME_COLORS (was falling through
  to muted default, now correctly shows red destructive badge)
- Keep BLOCKED in color map for compatibility with older NAP versions
## WAF Dashboard (new page)
- New full WAF Dashboard page with 6 tabs: Main, Bots, Advanced, Event Logs, Support ID, Custom
- Main tab: NIM-parity panel layout — All Web Attacks / Bot Attacks (donut) / Threat Intelligence
  cards with footer navigation links; Attack Requests Over Time with Hits/Blocked toggle;
  Top Attack Geolocations (placeholder), Top Violations (bar, clickable → Event Logs),
  Top WAF Policies (table, links to WAF Policies page), Top Attack IP Addresses (table, click-
  through to Event Logs), Top Attack URIs (table, click-through), Request Methods / Severity
  (donut charts), Response Codes placeholder, Top Signatures (table, click-through)
- Cardinality 'Showing N of N' footer on all Top* table panels
- Info tooltips (i icon) on every panel with NIM-sourced copy (Name/Description/Start/End/Filters)
  rendered via InfoTooltipQueryWindowProvider context so the time window is always accurate
- Bots tab: 7 NIM-titled placeholder panels (Bot Attack Requests, Bot Hits Over Time,
  Top Bot Categories/Classes/Signatures, Bot Applications)
- Advanced tab: real Top Attacked Instances panel (grouped by vs_name from ClickHouse),
  placeholder panels for Top Signature CVEs / Top Threat Campaigns / Violation Context, Outcomes
- Event Logs tab: SecurityLogsTab with clickable rows, IP/URI/attack-type filters, CSV export;
  pre-filters applied when navigating from dashboard panels (key prop remounts on filter change)
- Support ID tab: lookup by WAF support ID against ClickHouse
- Custom tab: fully user-configurable custom dashboard panels (CRUD) with named tabs;
  add/rename/delete custom tabs; each tab holds its own isolated panel set; Edit Panels toggle
  moved into Custom tab only (where it is relevant); zero-tab empty state with Add Tab CTA
- 'Custom analytics panels available in the Custom tab' notice on all analytics tabs
- Auto-refresh selector (Off/10s/30s/1m/5m/15m) with live countdown display
- Global time-range selector (1h/24h/7d/30d) applied to all panels

## WAF Policies page (major enhancements)
- NIM-style pill filter toolbar: Add Filter dropdown (field → value → Apply/Cancel),
  active filter pills with X, Clear all; filter fields: name, enforcement-mode, bundle-state
- Vertical-tab Create/Edit sheets at 75vw (APPolicy, APLogProfile, APUserSig forms rewritten)
  with WafWizardFrame vertical sidebar navigation replacing old horizontal step wizard
- Policy detail sheet: 3-tab layout (Details / Policy JSON / Security Logs) with badge display,
  Active Protections summary, Compilation info (SHA256/location), Signature Package Versions
- Deep-link support: ?policy=<name> query param auto-opens the matching policy's detail sheet
- Create button width normalised across all tabs (Policies/Log Profiles/User Signatures)
- pluralize() helper fixing 'policys' → 'policies' grammar bug
- enforcement-mode read from kebab-case field first (the real NAP field name)

## Backend additions
- services/clickhouse.py: ClickHouseService with HTTP client, auto-schema bootstrap (DDL),
  configurable via CLICKHOUSE_URL/USER/PASSWORD/DB env vars; DB name exported as CLICKHOUSE_DB
- routes/k8s/waf_dashboard.py: 11 analytics endpoints backed by ClickHouse:
  /waf/dashboard/status, /summary, /trend, /top-attacks, /top-ips, /top-uris,
  /top-policies, /request-methods, /severity, /top-signatures, /top-instances, /support-id
- routes/k8s/waf_panels.py: CRUD for custom dashboard panels + ClickHouse query execution
- routes/k8s/waf_dashboard_tabs.py: CRUD for user-defined custom tab groupings with
  self-healing migration (adopts legacy panels into a default tab on first access)
- routes/k8s/waf_logs.py: extended with optional cr_kind/cr_name (query all policies when
  omitted), added ip_filter and uri_filter query params, ClickHouse-first path
- models/waf_panels.py: WafPanel + WafDashboardTab SQLAlchemy models, panel query templates
  with configurable DB name via CLICKHOUSE_DB env var
- alembic v2_142: waf_events ClickHouse ingest cursor table
- alembic v2_143: waf_panels table for custom dashboard panels
- alembic v2_144: waf_dashboard_tabs table + tab_id column on waf_panels

## Frontend new files
- src/pages/WafDashboard.tsx (1500+ lines): full dashboard implementation
- src/lib/api/waf-dashboard.ts: API client for all dashboard endpoints
- src/lib/api/waf-dashboard-tabs.ts: API client for custom tab CRUD
- src/lib/api/waf-panels.ts: API client for custom panel CRUD + panel data queries
- src/hooks/useWafDashboard.ts: React Query hooks for all dashboard endpoints
- src/hooks/useWafPanels.ts: React Query hooks for panel CRUD
- src/hooks/useWafDashboardTabs.ts: React Query hooks for tab CRUD
- src/components/k8s/waf/InfoTooltip.tsx: self-contained info icon tooltip with
  QueryWindowProvider context for NIM-style Start/End/Filters metadata in every tooltip
- src/components/k8s/waf/WafPanelBuilderModal.tsx: Create/edit dialog for custom panels
- src/components/k8s/waf/WafPanelRenderer.tsx: Renders custom panels (bar/pie/area/line/kpi/table)

## Frontend changes
- tooltip.tsx: wrapped in TooltipPrimitive.Portal + normal-case (fixes font mismatch in tooltips)
- SecurityLogsTab.tsx: cr_kind/cr_name made optional (omit to query across all policies);
  added ip_filter, uri_filter inputs; pre-seed initial filter state from props (for click-through)
- Sidebar.tsx: WAF Dashboard added to navigation
- router.tsx: /waf-dashboard route registered

All ClickHouse DB/user/password values use env vars with sensible defaults (no hardcoded credentials).
Modifications to existing files to support the new WAF Dashboard and
improved WAF Policies pages:

- main.py: register new WAF routes (waf_dashboard, waf_panels, waf_dashboard_tabs)
- requirements.txt: add clickhouse-driver dependency
- celery_app.py: remove deprecated WAF ingest task (replaced by OTEL pipeline)
- services/clickhouse.py: ClickHouseService with configurable env vars, exports
  CLICKHOUSE_DB constant for use across route modules (avoids hardcoded DB name)
- routes/k8s/__init__.py: export new WAF route modules
- routes/k8s/waf_dashboard.py: 11 ClickHouse analytics endpoints with CLICKHOUSE_DB
  constant (env-configurable, no hardcoded DB name)
- routes/k8s/waf_logs.py: cr_kind/cr_name made optional (omit = query all policies);
  ip_filter and uri_filter query params added; CLICKHOUSE_DB constant used
- docker-compose.clickhouse.yml: uses env var references (${CLICKHOUSE_*}) instead
  of hardcoded credential values

- pages/WafPolicies.tsx: NIM-style pill filter toolbar, vertical-tab wizard shell,
  3-tab policy detail sheet, deep-link ?policy= support, consistent create button sizing
- pages/WafDashboard.tsx: full WAF Dashboard with all panels, tabs, tooltips,
  interactive click-through hyperlinks, custom tab system, Hits/Blocked toggle
- hooks/useWafDashboard.ts: React Query hooks for all 11 dashboard endpoints
- lib/api/waf-dashboard.ts: typed API client for all dashboard endpoints
- lib/api/waf-logs.ts: cr_kind/cr_name made optional in WafLogsParams
- components/k8s/waf/SecurityLogsTab.tsx: cr_kind/cr_name optional (dashboard mode
  queries all policies); ip_filter/uri_filter inputs; initial filter seeding from props
- components/k8s/waf/WafWizardFrame.tsx: rewritten with vertical left sidebar tabs
  replacing old horizontal step wizard
- components/k8s/waf/APPolicyForm.tsx: vertical-tab compatible, multi-tab form
- components/k8s/f5bnk-details/WafPolicyDetail.tsx: 3-tab detail view matching
  NIM layout (Details / Policy JSON / Security Logs)
- components/layout/Sidebar.tsx: WAF Dashboard nav entry added
- components/ui/tooltip.tsx: TooltipPrimitive.Portal wrapping + normal-case class
  (fixes font rendering when tooltip portal escapes styled containers)
- router.tsx: /waf-dashboard route added
Add complete Gateway API WAF management to WAF Policies page (5th tab: Gateway WAF).

## Binding Chain
APPolicy → F5BigWebSecurityProfile (policyName) → Gateway (annotation k8s.f5net.com/web-security-profile) ← HTTPRoute

## Backend (backend/routes/k8s/waf_gateway.py)
- Full CRUD for: GatewayClass, Gateway, F5BigWebSecurityProfile, HTTPRoute, ReferenceGrant
- Topology endpoint aggregating all resources
- WSP annotation applied automatically when creating/updating Gateways with WAF profile
- All existing k8s_resource_registry keys used (gateway, httproute, gatewayclass, referencegrant, f5-big-web-security-profiles)

## Frontend
- src/lib/api/waf-gateway.ts: TypeScript types and API client
- src/hooks/useWafGateway.ts: React Query hooks for all Gateway WAF resources
- src/pages/WafPolicies.tsx: GatewayWafTab with 5 sub-tabs:
  - Overview: resource count cards + binding chain diagram
  - Gateways: GatewayClass + Gateway CRUD with WAF profile selector
  - WAF Profiles: F5BigWebSecurityProfile CRUD with APPolicy dropdown
  - HTTP Routes: HTTPRoute CRUD with parent Gateway selector
  - Reference Grants: cross-namespace grant management

## E2E Verified
- GatewayClass f5-gatewayclass: Accepted via UI
- F5BigWebSecurityProfile ui-gateway-profile: created with policyName=bnk-waf-vs-policy
- Gateway ui-waf-gateway: Programmed=True, address 11.11.11.200 assigned, WAF annotation set
- HTTPRoute ui-echo-route: Accepted, 1 route attached to gateway
- Real traffic to 11.11.11.200:9080: HTTP 200 for legit, HTTP 400 for SQL injection
- TMM VS default-ui-waf-gateway-11.11.11.200-http-9080-vs: bind successful
_CLICKHOUSE_DB was imported only inside the endpoint function but used in
the module-level helper _read_logs_from_clickhouse(), causing NameError
and HTTP 500 on every Event Logs request.

Fix: import CLICKHOUSE_DB as _CLICKHOUSE_DB at module level and remove
the redundant inline import from the endpoint function.
Dashboard Filter bar:
- Outcome → Select (REJECTED/ALERTED/PASSED)
- Attack Type → Select populated from top-attacks live data
- Policy → Select populated from top-policies live data
- Virtual Server → Select populated from top-instances live data
- HTTP Method → Select (GET/POST/PUT/DELETE/PATCH/TRACE/OPTIONS)
- Client IP → kept as free text (too many distinct values)

Event Logs filter bar:
- Attack Type → Select populated from distinct values in current 200 entries
- IP address → Input with datalist autocomplete from top 30 IPs in entries
- URI → Input with datalist autocomplete from top 30 URIs in entries
- Virtual Server (f5virtualserver mode) → Select from distinct VS names
Move the Gateway API management feature from a tab inside WAF Policies to
a dedicated top-level page accessible from the sidebar under OPERATE.

Changes:
- New page: frontend-v2/src/pages/GatewayWAF.tsx
  - Standalone page with cluster/namespace pickers
  - All Gateway API functionality: GatewayClass, Gateway, WAF Profiles,
    HTTPRoutes, Reference Grants (5 sub-tabs)
  - Header reads 'Gateway API' / 'Gateway' — not WAF-specific
- router.tsx: added /gateway route
- Sidebar.tsx: added 'Gateway' entry in OPERATE section
- WafPolicies.tsx: removed Gateway WAF tab and all gateway-related imports
…ube deployment

## WAF Dashboard UI
- WafDashboard.tsx: replace three placeholder panels with live data:
  - Top Attack Geolocations: country bar chart from x_forwarded_for NAP field
  - Top Subviolations: horizontal bar chart from sub_violations NAP field
  - Response Codes: informational panel (NAP reports 0 for blocked requests)
- WafDashboard.tsx: fix Top Violations YAxis label truncation (28-char cutoff)
- WafDashboard.tsx: fix Top Attack URIs badge overflow (max 2 badges, truncated)
- index.html: remove Google Fonts runtime fetch; use system-ui fallback

## WAF Dashboard API (backend)
- waf_dashboard.py: add /top-subviolations endpoint (splits sub_violations field)
- waf_dashboard.py: add /top-geolocations endpoint (IP prefix → country lookup)
- waf_logs.py: fix f-string bug (SELECT raw_message/ingest_source → alias from waf_events)
- waf_logs.py: fix column names (waf_events has namespace/ingest_ts, not raw_message/ingest_source)

## API client & hooks
- waf-dashboard.ts: add getTopSubviolations and getTopGeolocations API methods
- useWafDashboard.ts: add useWafDashboardTopSubviolations and useWafDashboardTopGeolocations hooks

## OTel → ClickHouse pipeline (k8s/)
- waf-otel/otel-collector.yaml: syslog RFC3164 receiver, NAP field parse, ClickHouse export
- waf-syslog-receiver.yaml: TCP syslog debug receiver for testing
- waf-config.yaml: Gateway API CRs (APPolicy, APLogConf, F5BigWebSecurityProfile, SecPolicy, etc.)
- clickhouse/: ClickHouse deployment and bnkforge database setup

## Traffic generator (k8s/traffic-gen/)
- traffic-gen.py: modulated RPS (sinusoidal 0.5x-3x over 10-min periods)
- traffic-gen.py: base RPS 8, attack ratio 55%
- traffic-gen.py: alarm-only bot traffic (sqlmap, Nikto, masscan UAs) → ALERTED events
- traffic-gen.py: expanded attack payloads (LFI, encoded XSS, JSON injection, TRACE)
- traffic-gen.py: target fixed to 11.11.11.201:9080

## Helm chart
- helm/bnk-forge/templates/shared-pvcs.yaml: support global.sharedAccessMode override (RWO for local-path)
Brings the branch up to date with staging (was 29 commits behind), clearing
the stale-base CI reds flagged in mwiget's review:
- P4 Security Audit: staging's axios ^1.18.1 / js-yaml ^4.3.1 pins
- P4 Docker Build + Scan: staging's .trivyignore CVE-2026-57433 deferral (#104)

Conflicts resolved:
- .gitignore: kept staging's "package-lock.json is intentionally TRACKED"
  rationale (from #121) plus the !frontend-v2/package-lock.json un-ignore (union).
- frontend-v2/package-lock.json: took staging's package.json (newer secure pins)
  and REGENERATED the lockfile with `npm install` rather than hand-merging.

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
Commit 69a080c had dropped `self.MonacoEnvironment.getWorker` and the
`monaco-editor/esm/vs/editor/editor.worker?worker` import from main.tsx,
degrading Monaco's language services to the main thread and removing the
dedicated worker that the @monaco-editor/react loader does not supply.

Restore both. The stated reason for the deletion ("?worker not compatible
with all build configurations") does not hold in this repo: the ?worker
import is Vite's supported worker syntax and `npm run build` (tsc && vite
build) passes green with it — no vite.config.ts worker override needed.

Also reverts the unrelated blank-line-only diff in vite.config.ts.

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
Per AGENTS.md, routes carry `@handle_route_errors(...)` AND `response_model=`.
The WAF routes had neither declared (grep -c response_model was 0), so the
1026 lines added to openapi.json — and the generated TS types derived from
them — were untyped.

- New schemas/waf.py: response models matching each route's actual return
  shape (WafResource with extra="allow" for the verbatim CRD objects, list
  envelopes, delete/recompile/security-logs responses).
- waf_policies.py: response_model on all 17 routes.
- waf_logs.py: response_model on the security-logs route (+ split a combined
  clickhouse import so ruff isort passes).
- Regenerated backend/openapi.json (generate-openapi.py --check passes) and
  frontend-v2/src/types/api-generated.ts (openapi-typescript).

Auth decorators unchanged (require_viewer on reads, require_cluster_owner on
mutations — already correct per review).

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
…alog

The `autoFocus` added to the confirmation Input is a change to a shared UI
primitive used well beyond WAF, and is out of scope for this PR (flagged in
review). It is a hardcoded focus nicety, not a prop, so removing it cannot
break WAF functionality — reverted to match staging.

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
…aging merge

The staging merge surfaced four red P1 jobs that the changed-files-only local
checks missed (CI lints the whole tree and validates the migration chain vs
origin/staging):

- Lint Backend: `ruff check .` flagged 15 errors across the branch's own WAF
  routes/models (unsorted/unused imports, `if x: return` one-liners, ambiguous
  `l`, a dead `items = []`). All mechanical; behavior unchanged.
- Migration Collision: the merge left duplicate revision ids — the branch's WAF
  migrations reused v2_143/v2_144, already taken on staging. Renumbered the
  three WAF migrations to chain linearly after the current staging head v2_155
  (v2_156 ingestion-cursors -> v2_157 panels -> v2_158 dashboard-tabs). Validator
  now reports a single linear chain.
- Lint Frontend: eslint flagged restricted `useMutation` imports (3 WAF hooks now
  use `useAppMutation` for the default error toast), two D-020 raw-palette colors
  (text-blue-500 -> text-info, bg-orange-400 -> bg-warning), and a ternary used as
  a statement (-> if/else).
- Unit Tests · Frontend: the SecurityLogsTab test still asserted the old
  free-text `vs_name…` input; the vs filter is now a Select. Updated the
  assertion to the rendered "All virtual servers" option.

ruff clean, `make check-migrations` green (single linear chain), eslint 0 errors,
full frontend suite green (2609 passed), backend WAF tests 34 passed.

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
…ostgres upgrade)

The P2 "Migration Upgrade From Released Version (Postgres)" job — which only
started running once the migration-collision gate went green — failed with
`relation "k8s_clusters" does not exist`. waf_panels FK-referenced
`k8s_clusters.id`, but the clusters table is `kubernetes_clusters`; SQLite
doesn't validate FK targets at CREATE time so it was never caught locally, and
the previously-red collision gate had skipped this Postgres job entirely.

Rather than repoint the FK, drop it: the sibling WAF tables (waf_ingestion_cursors,
waf_dashboard_tabs) carry a plain indexed `cluster_id` with no FK, and
v2_158's own comment states "no FK to keep schema portable, matches waf_panels"
— i.e. the author's documented intent was for waf_panels to have no cluster FK.
This makes the three WAF tables consistent and matches the ORM model (which
declares cluster_id as a plain Integer, no relationship/FK). The valid
created_by -> users.id FK is unchanged.

check-migrations: single linear chain; ruff clean.

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
…tion chain

The "P2 · Migration Upgrade From Released Version (Postgres)" job builds one
DB with create_all at HEAD and another via the migration chain, then runs
scripts/check-schema-parity.py to assert they agree. Three WAF tables were
chain_only — created by the migration chain but never by create_all — which
hard-fails the parity gate:
  - waf_panels
  - waf_dashboard_tabs
  - waf_ingestion_cursors

Root cause: models.WafPanel/WafDashboardTab existed in models/waf_panels.py
but were never imported by models/__init__.py, so Base.metadata (and thus
create_all) never registered them. There was no ORM model at all for
waf_ingestion_cursors — it lived only in migration v2_156.

Fix:
  - Import WafPanel and WafDashboardTab in models/__init__.py (+ __all__).
  - Add a WafIngestionCursor model mirroring v2_156 exactly (columns, types,
    nullability, the uq_waf_cursor_cluster_file unique constraint and the
    ix_waf_ingestion_cursors_cluster_id index).
  - Reconcile WafPanel/WafDashboardTab with their migrations (v2_157/v2_158):
    add the missing created_by column and the cluster indexes
    (ix_waf_panels_cluster_id, idx_waf_panels_cluster,
    idx_waf_dashboard_tabs_cluster) so importing them introduces no new
    column/index drift.

Verified locally against postgres:16 by provisioning the chain DB at the
MIN_UPGRADE_FROM floor (v3.1.6) and upgrading to HEAD, then create_all at
HEAD for the ORM side: check-schema-parity.py exits 0 ("create_all and the
migration chain agree on every table, column and index"). No WAF entries were
added to _KNOWN_INDEX_DRIFT / _KNOWN_NULLABILITY_DRIFT. make check-migrations
stays a single linear chain; WAF unit tests pass; ruff clean.

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
… default

The new WafIngestionCursor.updated_at used server_default=text("now()"), which
emits a literal `DEFAULT now()` under every dialect. On SQLite — which the unit
and contract test suites build via Base.metadata.create_all — `now()` is not a
valid default function, so CREATE TABLE failed with `near "(": syntax error`,
erroring out 239 tests that share the create_all fixture (Unit Tests · Backend
and Contract Tests, both green before the model was registered).

func.now() (as the sibling WafPanel/WafDashboardTab models already use) renders
as CURRENT_TIMESTAMP on SQLite and now() on Postgres, so create_all works and the
Postgres migration-parity result is unchanged. Dropped the now-unused text import.

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
@jgruberf5

Copy link
Copy Markdown
Collaborator

Review + resolution of the CHANGES_REQUESTED items @ 55465e1c

Picking this up to get it back to a reviewable state. I've addressed every blocker from your two reviews, merged staging, and fixed several CI failures that only surfaced once the branch was current. Mapping each of your points to what changed:

Your blockers

Blocker (from 7229cd0 / c91f84f) Status
🔴 Monaco worker wiring removed (air-gap regression) Restored. frontend-v2/src/main.tsx again wires self.MonacoEnvironment.getWorker with the monaco-editor/esm/vs/editor/editor.worker?worker import, alongside the retained loader.config({ monaco }). The worker runs off the main thread again; nothing falls back to CDN. Your own note confirmed the ?worker import builds in this repo's CI, so no vite.config.ts workaround was needed — the deletion, not the import, was the problem.
🟠 PR description was the unedited template Rewritten. Real description, Implements: #136 linked, and an explicit note that later commits grew a WAF dashboard + Gateway page beyond the original Policy-Manager scope (your scope point, surfaced rather than hidden).
🟡 No response_model on the routes Fixed. 17 response_model= on waf_policies.py and 1 on waf_logs.py, with new Pydantic schemas in backend/schemas/waf.py; openapi.json + api-generated.ts regenerated so the generated types are real.
🔴 CI red (staleness) Merged staging (.gitignore union; package-lock.json regenerated via npm install off staging's package.json, keeping axios 1.18.1 / js-yaml 4.3.1 — not hand-merged). Then fixed the failures below.
⚪ Unrelated changes destructive-confirm-dialog.tsx autoFocus reverted; vite.config.ts blank line reverted. (useTaskWebSocket.ts window.setTimeout is a correct typing fix and is retained.)

CI failures found + fixed after the merge (whole-tree gates the changed-files checks missed)

  1. Lint Backend — 15 ruff errors in the branch's own WAF routes/models (unsorted/unused imports, if x: return one-liners, ambiguous l, a dead items = []). Mechanical, no behavior change; ruff now clean.
  2. Migration Collision — the staging merge left duplicate revision ids (the WAF migrations reused v2_143/v2_144, already taken on staging). Renumbered them to chain linearly after the current staging head v2_155v2_156 (ingestion-cursors) → v2_157 (panels) → v2_158 (dashboard-tabs). Single linear chain.
  3. Migration Upgrade From Released (Postgres) — two real schema bugs that were masked while the collision gate stayed red and skipped this Postgres job:
    • waf_panels FK-referenced a non-existent table k8s_clusters (the clusters table is kubernetes_clusters); SQLite doesn't validate FK targets at CREATE so it was never caught locally. Dropped the FK to match the sibling WAF tables' plain indexed cluster_id and v2_158's own "no FK to keep schema portable, matches waf_panels" comment.
    • The three WAF tables were chain-only (created by migrations but not by create_all): WafPanel/WafDashboardTab were never imported in models/__init__.py, and waf_ingestion_cursors had no ORM model. Registered all three and reconciled the models to their migrations exactly (added the missing created_by column + the cluster indexes, added a WafIngestionCursor model). check-schema-parity.py now agrees on every table, column and index.
  4. Lint Frontend — restricted useMutation imports (3 WAF hooks now use useAppMutation), two D-020 raw-palette colors (text-blue-500text-info, bg-orange-400bg-warning), a ternary-as-statement → if/else.
  5. Unit Tests · Frontend — the SecurityLogsTab test still asserted the old free-text vs_name… input; the vs filter is now a <Select>. Updated to assert the rendered "All virtual servers" option. Full frontend suite green (2609 passed); backend WAF tests 34 passed.

Remaining red: the two repo-wide P4 security gates (not this PR's code)

P4 · Docker Build + Scan (Trivy CRITICAL CVE-2026-56854, x/crypto/ssh in helm 3.20.0) and P4 · Security Audit (gitpython 3.1.58) are environmental — the advisory/vuln DBs updated after staging last audited clean on 2026-08-24, so every open PR and staging itself would be red. Both are fixed in #215 (documented .trivyignore suppression for the un-fixable-yet helm CVE + a gitpython patch bump); this branch goes green on P4 once #215 merges and it rebases.

Still owed by the author (I deliberately did not invent these)

  • ADR. #136 is now linked, but I won't fabricate an ADR / design record for a feature I didn't design — if AGENTS.md requires a formal ADR for something this size, that's yours to write.
  • The scope-split decision. The dashboard / Gateway page / syslog-receiver that grew on top of the original Policy-Manager scope is a maintainer call — I've surfaced it in the description rather than unilaterally removing working features.

I can't dismiss the CHANGES_REQUESTED (that's yours), but the four blockers are addressed and the branch is green on P1–P3. Requesting re-review of the wizard/forms/route logic against #136, with the ADR/scope-split as the two items still needing your/the author's decision.

https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW

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.

4 participants