Skip to content

feat(free,#326): add a local protection report - #338

Merged
MrDarkRoot merged 3 commits into
mainfrom
feat/326-protection-report
Nov 11, 2026
Merged

MrDarkRoot merged 3 commits into
mainfrom
feat/326-protection-report

Conversation

@MrDarkRoot

Copy link
Copy Markdown
Owner

Scope

Adds a read-only Protection report under Products → Protection, next to Bulk Prices and Grants. Fixes nothing in the perimeter or the grant store. Does not close #326.

Invariant / Current Spec Reference

Denial and grant enforcement are unchanged. Perimeter_Guard and Perimeter_Audit_Log::record() were not edited. Grant writes were not edited. Version, Stable tag, and the 0.3.0 tag were not bumped. readme.txt and docs/marketing/** were not edited.

Relevant Canonical CC-* Tests (docs/test-plan.md)

No legacy CC-* experiment ID. New hermetic test: php wordpress/tests/admin/protection-report-unit.php.

What Changed

  • Protection_Report_Query reads two existing stores.
  • Protection_Report_Admin renders the window (7/30/90), blocked-attempt counts by the surfaces the denial log actually stores, active grants (used, reserved, remaining, expiry), and recent refusal rows (time, actor id and display name, surface, product id, reason code).
  • Same capability gate as grant management: Grant_Admin::can_mutate() (manage_woocommerce and edit_products).
  • Output is escaped. The window, page, log read, and grant list are bounded. Request bodies are not shown.

No new table

The issue guessed a writeleash_perimeter_* table. That table does not exist.

Perimeter refusals are append-only JSON lines written by Perimeter_Audit_Log when WP_DEBUG_LOG is enabled. Each line stores timestamp, code, where, actor, object_id, kind, and detail. The surface is where (rest, product_save, variation_save, product_set_stock, variation_set_stock, update_product_stock_query, update_post_metadata, add_post_metadata, delete_post_metadata, pre_trash_post, pre_delete_post).

writeleash_grant_audit cannot answer "denials by surface". It has no surface column, and perimeter refusals are not inserted there. Active budget and expiry already live on writeleash_grants (budget_max, budget_consumed, budget_reserved, expires_at, status, restriction_removed_at). Those two stores answer the report, so no table was added.

detail and route are dropped at parse time. REST lines store the actor user id in object_id; the screen says so. Other surfaces store the product id.

The empty state says what appears after a restricted account is used. If WP_DEBUG_LOG is off, the screen says refusals are not recorded yet.

Security Impact

Read-only. No wp_remote_, curl_, or file_get_contents. No telemetry. Prepared SQL only (%i for the grants table). Actor display is user id plus display name, not email or login. A restricted account's direct product write is still denied by the existing guard; this screen only reads the line the guard already asked the audit log to append.

PostgreSQL Semantics Considered

Not applicable. This screen reads the WordPress debug log and the existing MySQL/MariaDB grant table. No PostgreSQL behavior is claimed.

Adversarial / Bypass Testing

php wordpress/tests/admin/protection-report-unit.php

Result: #326 protection report unit: PASS (143 assertions). The run covers the empty state, 7/30/90 windows, surface reconciliation, pagination caps, grant budget remaining (including a non-negative floor), expired/revoked/removed grants, and a fixture log whose detail and route do not survive parsing. The source assertion checks both new classes for wp_remote_, curl_, file_get_contents, and http:// / https://.

bash .github/ci/pr-fast.sh on 141d439598890e7b446f111b0f6ae7c2ac602df1: #133 PR_FAST PASS.

Limitations / Unknowns

  • Refusal lines exist only when WP_DEBUG_LOG is enabled, and the audit log stops appending at 5 MB. The report cannot count a refusal that was never written.
  • The log read is capped at 5 MB from the end and 5000 denial lines. The table shows at most 400 of those (20 pages of 20).
  • REST object_id is the actor id because that is what Perimeter_Guard::rest_pre_dispatch() logs. This PR does not change that call.
  • Docker was not required for this PR. The disposable demo is a separate branch.

Out-of-Scope Work Explicitly Not Included

  • I did not silently expand the support envelope.
  • I updated the current spec, test plan, threat model, or decisions if behavior changed. Behavior of denial and grants did not change. PUBLIC-PAYLOAD.md records the new read-only screen because the package inventory requires it.
  • Security-sensitive behavior has adversarial test coverage. The new code is read-only; the unit test includes the network-source assertion. Enforcement tests were not weakened.
  • No secrets or credentials are included.

Read blocked attempts from the perimeter denial log and active grant
budget from the existing grants table. No new table and no change to
denial or grant enforcement.
Plugin Check rejects fopen and fclose. The report now uses the
filesystem API and does not load a log past the byte cap.
@MrDarkRoot

Copy link
Copy Markdown
Owner Author

Plugin Check failed on fopen / fclose in class-protection-report-query.php (run 42809216793, job #62/#63 mysql). Pushed f233f8ba15b3f812dff691022df3e89eb7772af0.

The reader no longer calls fopen or fclose. It uses WP_Filesystem::size() and WP_Filesystem::get_contents(), and it does not load a log larger than the byte cap. php wordpress/tests/admin/protection-report-unit.php — #326 protection report unit: PASS (150 assertions). Plugin Check was not re-run locally; only that unit test and package preflight were.

The adapter / redirection-adapter failure in the same run (trusted barrier overlap proof failed, await_overlap(update) timed out) is NOT caused by the report. Protection_Report_Admin::boot() only registers admin_menu. Denial and grant enforcement were not changed to chase that timeout.

@MrDarkRoot

Copy link
Copy Markdown
Owner Author

QA verdict: BLOCKED at f233f8ba15b3f812dff691022df3e89eb7772af0.

Mergeable, not behind, GitHub merge state BLOCKED. Scope is a local Protection screen. class-protection-report-admin.php only registers admin_menu and renders. The render gate is Grant_Admin::can_mutate() (manage_woocommerce and edit_products). The menu capability argument is the weaker edit_products; the callback still returns before output. Queries in class-protection-report-query.php are SELECT only. Denial lines come from the local WP_DEBUG_LOG file. detail and route are dropped. No wp_remote_ or curl_ in the two new classes. Booting the screen does not change Perimeter_Guard decisions.

Head 141d439 failed historical Plugin Check on fopen/fclose in class-protection-report-query.php (exit 255, unclassified errors). f233f8b reads through WP_Filesystem instead. On this head, historical and the redirection adapter succeeded, and PR_FAST succeeded. admin, jobs, journal, and undo were still in progress at 2026-11-10 12:10 UTC, so CI_COVERAGE was not green. Acceptance stayed skipped. A log larger than the byte cap now returns no rows (capped: true) instead of the tail. wordpress/tests/admin/protection-report-unit.php was not run locally. NOT_TESTED.

Do not merge until admin, jobs, journal, and undo finish on this exact SHA.

@MrDarkRoot
MrDarkRoot merged commit 3931390 into main Nov 11, 2026
16 checks passed
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.

product(seatbelt): add a local "Protection report" screen — visible proof without telemetry

1 participant